using System.Buffers; using System.Buffers.Text; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; /// /// Represents content within the Model Context Protocol (MCP). /// /// /// /// The class is a fundamental type in the MCP that can represent different forms of content /// based on the property. Derived types like , , /// and provide the type-specific content. /// /// /// This class is used throughout the MCP for representing content in messages, tool responses, /// and other communication between clients and servers. /// /// /// See the schema for more details. /// /// [JsonConverter(typeof(Converter))] public abstract class ContentBlock { /// Prevent external derivations. private protected ContentBlock() { } /// /// When overridden in a derived class, gets the type of content. /// /// /// The type of content. Valid values include "image", "audio", "text", "resource", "resource_link", "tool_use", and "tool_result". /// /// /// This value determines the structure of the content object. /// [JsonPropertyName("type")] public abstract string Type { get; } /// /// Gets or sets optional annotations for the content. /// /// /// These annotations can be used to specify the intended audience (, , or both) /// and the priority level of the content. Clients can use this information to filter or prioritize content for different roles. /// [JsonPropertyName("annotations")] public Annotations? Annotations { get; set; } /// /// Gets or sets metadata reserved by MCP for protocol-level metadata. /// /// /// Implementations must not make assumptions about its contents. /// [JsonPropertyName("_meta")] public JsonObject? Meta { get; set; } /// /// Provides a for . /// /// Provides a polymorphic converter for the class that doesn't require /// setting explicitly. [EditorBrowsable(EditorBrowsableState.Never)] public sealed class Converter : JsonConverter { /// public override ContentBlock? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) { return null; } if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException(); } string? type = null; string? text = null; string? name = null; string? title = null; ReadOnlyMemory? data = null; string? mimeType = null; string? uri = null; string? description = null; long? size = null; IList? icons = null; ResourceContents? resource = null; Annotations? annotations = null; JsonObject? meta = null; string? id = null; JsonElement? input = null; string? toolUseId = null; List? content = null; JsonElement? structuredContent = null; bool? isError = null; while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { if (reader.TokenType != JsonTokenType.PropertyName) { continue; } string? propertyName = reader.GetString(); bool success = reader.Read(); Debug.Assert(success, "STJ must have buffered the entire object for us."); switch (propertyName) { case "type": type = reader.GetString(); break; case "text": text = reader.GetString(); break; case "name": name = reader.GetString(); break; case "title": title = reader.GetString(); break; case "data": data = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray(); break; case "mimeType": mimeType = reader.GetString(); break; case "uri": uri = reader.GetString(); break; case "description": description = reader.GetString(); break; case "size": size = reader.GetInt64(); break; case "icons": if (reader.TokenType == JsonTokenType.StartArray) { icons = []; while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) { icons.Add(JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.Icon) ?? throw new JsonException("Unexpected null item in icons array.")); } } break; case "resource": resource = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.ResourceContents); break; case "annotations": annotations = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.Annotations); break; case "_meta": meta = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonObject); break; case "id": id = reader.GetString(); break; case "input": input = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonElement); break; case "toolUseId": toolUseId = reader.GetString(); break; case "content": if (reader.TokenType == JsonTokenType.StartArray) { content = []; while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) { content.Add(Read(ref reader, typeof(ContentBlock), options) ?? throw new JsonException("Unexpected null item in content array.")); } } else { content = [Read(ref reader, typeof(ContentBlock), options) ?? throw new JsonException("Unexpected null content item.")]; } break; case "structuredContent": structuredContent = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonElement); break; case "isError": isError = reader.GetBoolean(); break; default: reader.Skip(); break; } } ContentBlock block = type switch { "text" => new TextContentBlock { Text = text ?? throw new JsonException("Text contents must be provided for 'text' type."), }, "image" => new ImageContentBlock { Data = data ?? throw new JsonException("Image data must be provided for 'image' type."), MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'image' type."), }, "audio" => new AudioContentBlock { Data = data ?? throw new JsonException("Audio data must be provided for 'audio' type."), MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'audio' type."), }, "resource" => new EmbeddedResourceBlock { Resource = resource ?? throw new JsonException("Resource contents must be provided for 'resource' type."), }, "resource_link" => new ResourceLinkBlock { Uri = uri ?? throw new JsonException("URI must be provided for 'resource_link' type."), Name = name ?? throw new JsonException("Name must be provided for 'resource_link' type."), Title = title, Description = description, MimeType = mimeType, Size = size, Icons = icons, }, "tool_use" => new ToolUseContentBlock { Id = id ?? throw new JsonException("ID must be provided for 'tool_use' type."), Name = name ?? throw new JsonException("Name must be provided for 'tool_use' type."), Input = input ?? throw new JsonException("Input must be provided for 'tool_use' type."), }, "tool_result" => new ToolResultContentBlock { ToolUseId = toolUseId ?? throw new JsonException("ToolUseId must be provided for 'tool_result' type."), Content = content ?? throw new JsonException("Content must be provided for 'tool_result' type."), StructuredContent = structuredContent, IsError = isError, }, _ => throw new JsonException($"Unknown content type: '{type}'"), }; block.Annotations = annotations; block.Meta = meta; return block; } /// public override void Write(Utf8JsonWriter writer, ContentBlock value, JsonSerializerOptions options) { if (value is null) { writer.WriteNullValue(); return; } writer.WriteStartObject(); writer.WriteString("type", value.Type); switch (value) { case TextContentBlock textContent: writer.WriteString("text", textContent.Text); break; case ImageContentBlock imageContent: writer.WriteString("data", imageContent.Data.Span); writer.WriteString("mimeType", imageContent.MimeType); break; case AudioContentBlock audioContent: writer.WriteString("data", audioContent.Data.Span); writer.WriteString("mimeType", audioContent.MimeType); break; case EmbeddedResourceBlock embeddedResource: writer.WritePropertyName("resource"); JsonSerializer.Serialize(writer, embeddedResource.Resource, McpJsonUtilities.JsonContext.Default.ResourceContents); break; case ResourceLinkBlock resourceLink: writer.WriteString("uri", resourceLink.Uri); writer.WriteString("name", resourceLink.Name); if (resourceLink.Title is not null) { writer.WriteString("title", resourceLink.Title); } if (resourceLink.Description is not null) { writer.WriteString("description", resourceLink.Description); } if (resourceLink.MimeType is not null) { writer.WriteString("mimeType", resourceLink.MimeType); } if (resourceLink.Size.HasValue) { writer.WriteNumber("size", resourceLink.Size.Value); } if (resourceLink.Icons is { Count: > 0 }) { writer.WritePropertyName("icons"); writer.WriteStartArray(); foreach (var icon in resourceLink.Icons) { JsonSerializer.Serialize(writer, icon, McpJsonUtilities.JsonContext.Default.Icon); } writer.WriteEndArray(); } break; case ToolUseContentBlock toolUse: writer.WriteString("id", toolUse.Id); writer.WriteString("name", toolUse.Name); writer.WritePropertyName("input"); JsonSerializer.Serialize(writer, toolUse.Input, McpJsonUtilities.JsonContext.Default.JsonElement); break; case ToolResultContentBlock toolResult: writer.WriteString("toolUseId", toolResult.ToolUseId); writer.WritePropertyName("content"); writer.WriteStartArray(); foreach (var item in toolResult.Content) { Write(writer, item, options); } writer.WriteEndArray(); if (toolResult.StructuredContent.HasValue) { writer.WritePropertyName("structuredContent"); JsonSerializer.Serialize(writer, toolResult.StructuredContent.Value, McpJsonUtilities.JsonContext.Default.JsonElement); } if (toolResult.IsError.HasValue) { writer.WriteBoolean("isError", toolResult.IsError.Value); } break; } if (value.Annotations is { } annotations) { writer.WritePropertyName("annotations"); JsonSerializer.Serialize(writer, annotations, McpJsonUtilities.JsonContext.Default.Annotations); } if (value.Meta is not null) { writer.WritePropertyName("_meta"); JsonSerializer.Serialize(writer, value.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); } writer.WriteEndObject(); } } } /// Represents text provided to or from an LLM. [DebuggerDisplay("Text = \"{Text}\"")] public sealed class TextContentBlock : ContentBlock { /// public override string Type => "text"; /// /// Gets or sets the text content of the message. /// [JsonPropertyName("text")] public required string Text { get; set; } /// public override string ToString() => Text ?? ""; } /// Represents an image provided to or from an LLM. [DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ImageContentBlock : ContentBlock { private ReadOnlyMemory? _decodedData; private ReadOnlyMemory _data; /// /// Creates an from decoded image bytes. /// /// The unencoded image bytes. /// The MIME type of the image. /// A new instance. /// /// This method stores the provided bytes as and encodes them to base64 UTF-8 bytes for . /// /// is . /// is empty or composed entirely of whitespace. public static ImageContentBlock FromBytes(ReadOnlyMemory bytes, string mimeType) { Throw.IfNullOrWhiteSpace(mimeType); ReadOnlyMemory data = EncodingUtilities.EncodeToBase64Utf8(bytes); return new() { _decodedData = bytes, Data = data, MimeType = mimeType }; } /// public override string Type => "image"; /// /// Gets or sets the base64-encoded UTF-8 bytes representing the image data. /// /// /// Setting this value will invalidate any cached value of . /// [JsonPropertyName("data")] public required ReadOnlyMemory Data { get => _data; set { _data = value; _decodedData = null; // Invalidate cache } } /// /// Gets the decoded image data represented by . /// /// /// /// When getting, this member will decode the value in and cache the result. /// Subsequent accesses return the cached value unless is modified. /// /// [JsonIgnore] public ReadOnlyMemory DecodedData { get { if (_decodedData is null) { _decodedData = EncodingUtilities.DecodeFromBase64Utf8(Data); } return _decodedData.Value; } } /// /// Gets or sets the MIME type (or "media type") of the content, specifying the format of the data. /// /// /// Common values include "image/png" and "image/jpeg". /// [JsonPropertyName("mimeType")] public required string MimeType { get; set; } [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"MimeType = {MimeType}, Length = {DebuggerDisplayHelper.GetBase64LengthDisplay(Data)}"; } /// Represents audio provided to or from an LLM. [DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class AudioContentBlock : ContentBlock { private ReadOnlyMemory? _decodedData; private ReadOnlyMemory _data; /// /// Creates an from decoded audio bytes. /// /// The unencoded audio bytes. /// The MIME type of the audio. /// A new instance. /// /// This method stores the provided bytes as and encodes them to base64 UTF-8 bytes for . /// /// is . /// is empty or composed entirely of whitespace. public static AudioContentBlock FromBytes(ReadOnlyMemory bytes, string mimeType) { Throw.IfNullOrWhiteSpace(mimeType); ReadOnlyMemory data = EncodingUtilities.EncodeToBase64Utf8(bytes); return new() { _decodedData = bytes, Data = data, MimeType = mimeType }; } /// public override string Type => "audio"; /// /// Gets or sets the base64-encoded UTF-8 bytes representing the audio data. /// /// /// Setting this value will invalidate any cached value of . /// [JsonPropertyName("data")] public required ReadOnlyMemory Data { get => _data; set { _data = value; _decodedData = null; // Invalidate cache } } /// /// Gets the decoded audio data represented by . /// /// /// /// When getting, this member will decode the value in and cache the result. /// Subsequent accesses return the cached value unless is modified. /// /// [JsonIgnore] public ReadOnlyMemory DecodedData { get { if (_decodedData is null) { _decodedData = EncodingUtilities.DecodeFromBase64Utf8(Data); } return _decodedData.Value; } } /// /// Gets or sets the MIME type (or "media type") of the content, specifying the format of the data. /// /// /// Common values include "audio/wav" and "audio/mp3". /// [JsonPropertyName("mimeType")] public required string MimeType { get; set; } [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"MimeType = {MimeType}, Length = {DebuggerDisplayHelper.GetBase64LengthDisplay(Data)}"; } /// Represents the contents of a resource, embedded into a prompt or tool call result. /// /// It is up to the client how best to render embedded resources for the benefit of the LLM and/or the user. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class EmbeddedResourceBlock : ContentBlock { /// public override string Type => "resource"; /// /// Gets or sets the resource content of the message when is "resource". /// /// /// /// Resources can be either text-based () or /// binary (), allowing for flexible data representation. /// Each resource has a URI that can be used for identification and retrieval. /// /// [JsonPropertyName("resource")] public required ResourceContents Resource { get; set; } [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"Uri = \"{Resource.Uri}\""; } /// Represents a resource that the server is capable of reading, included in a prompt or tool call result. /// /// Resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests. /// [DebuggerDisplay("Name = {Name}, Uri = \"{Uri}\"")] public sealed class ResourceLinkBlock : ContentBlock { /// public override string Type => "resource_link"; /// /// Gets or sets the URI of this resource. /// [JsonPropertyName("uri")] [StringSyntax(StringSyntaxAttribute.Uri)] public required string Uri { get; set; } /// /// Gets or sets a human-readable name for this resource. /// [JsonPropertyName("name")] public required string Name { get; set; } /// /// Gets or sets a title for this resource. /// /// /// This is intended for UI and end-user contexts. It is optimized to be human-readable and easily understood, /// even by those unfamiliar with domain-specific terminology. /// If not provided, can be used for display. /// [JsonPropertyName("title")] public string? Title { get; set; } /// /// Gets or sets a description of what this resource represents. /// /// /// /// This description can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a \"hint\" to the model. /// /// /// The description should provide clear context about the resource's content, format, and purpose. /// This helps AI models make better decisions about when to access or reference the resource. /// /// /// Client applications can also use this description for display purposes in user interfaces /// or to help users understand the available resources. /// /// [JsonPropertyName("description")] public string? Description { get; set; } /// /// Gets or sets the MIME type of this resource. /// /// /// /// specifies the format of the resource content, helping clients to properly interpret and display the data. /// Common MIME types include "text/plain" for plain text, "application/pdf" for PDF documents, /// "image/png" for PNG images, and "application/json" for JSON data. /// /// /// This property can be if the MIME type is unknown or not applicable for the resource. /// /// [JsonPropertyName("mimeType")] public string? MimeType { get; set; } /// /// Gets or sets the size of the raw resource content (before base64 encoding), in bytes, if known. /// /// /// This value can be used by applications to display file sizes and estimate context window usage. /// [JsonPropertyName("size")] public long? Size { get; set; } /// /// Gets or sets an optional list of icons for this resource. /// /// /// This can be used by clients to display the resource's icon in a user interface. /// [JsonPropertyName("icons")] public IList? Icons { get; set; } } /// Represents a request from the assistant to call a tool. [DebuggerDisplay("Name = {Name}, Id = {Id}")] public sealed class ToolUseContentBlock : ContentBlock { /// public override string Type => "tool_use"; /// /// Gets or sets a unique identifier for this tool use. /// /// /// This ID is used to match tool results to their corresponding tool uses. /// [JsonPropertyName("id")] public required string Id { get; set; } /// /// Gets or sets the name of the tool to call. /// [JsonPropertyName("name")] public required string Name { get; set; } /// /// Gets or sets the arguments to pass to the tool, conforming to the tool's input schema. /// [JsonPropertyName("input")] public required JsonElement Input { get; set; } } /// Represents the result of a tool use, provided by the user back to the assistant. [DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ToolResultContentBlock : ContentBlock { /// public override string Type => "tool_result"; /// /// Gets or sets the ID of the tool use this result corresponds to. /// /// /// This value must match the ID from a previous . /// [JsonPropertyName("toolUseId")] public required string ToolUseId { get; set; } /// /// Gets or sets the unstructured result content of the tool use. /// /// /// This value has the same format as CallToolResult.Content and can include text, images, /// audio, resource links, and embedded resources. /// [JsonPropertyName("content")] public required IList Content { get; set; } /// /// Gets or sets an optional structured result object. /// /// /// If the tool defined an outputSchema, this object should conform to that schema. /// [JsonPropertyName("structuredContent")] public JsonElement? StructuredContent { get; set; } /// /// Gets or sets a value that indicates whether the tool use resulted in an error. /// /// /// if the tool use resulted in an error; if it succeeded. The default is . /// /// /// If , the content typically describes the error that occurred. /// [JsonPropertyName("isError")] public bool? IsError { get; set; } [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay { get { if (IsError == true) { return $"ToolUseId = {ToolUseId}, IsError = true"; } // Try to show the result content if (Content.Count == 1 && Content[0] is TextContentBlock textBlock) { return $"ToolUseId = {ToolUseId}, Result = \"{textBlock.Text}\""; } if (StructuredContent.HasValue) { try { string json = StructuredContent.Value.GetRawText(); return $"ToolUseId = {ToolUseId}, Result = {json}"; } catch { // Fall back to content count if GetRawText fails } } return $"ToolUseId = {ToolUseId}, ContentCount = {Content.Count}"; } } }