From 1b52ddacba386130d21ceb8651a5236e520d48b9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:09:46 -0800 Subject: [PATCH] Add DebuggerDisplay attributes to Protocol types (#1068) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Stephen Halter --- .../Protocol/BlobResourceContents.cs | 13 +++++ .../Protocol/ContentBlock.cs | 52 +++++++++++++++++++ .../Protocol/DebuggerDisplayHelper.cs | 30 +++++++++++ .../Protocol/Icon.cs | 12 +++++ .../Protocol/Prompt.cs | 12 +++++ .../Protocol/PromptMessage.cs | 17 ++++++ .../Protocol/Resource.cs | 2 + .../Protocol/SamplingMessage.cs | 20 +++++++ .../Protocol/TextResourceContents.cs | 5 ++ .../Protocol/Tool.cs | 12 +++++ 10 files changed, 175 insertions(+) create mode 100644 src/ModelContextProtocol.Core/Protocol/DebuggerDisplayHelper.cs diff --git a/src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs b/src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs index dc56dac..904e71b 100644 --- a/src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs +++ b/src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -20,6 +21,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for more details. /// /// +[DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class BlobResourceContents : ResourceContents { /// @@ -27,4 +29,15 @@ public sealed class BlobResourceContents : ResourceContents /// [JsonPropertyName("blob")] public required string Blob { get; set; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + { + get + { + string lengthDisplay = DebuggerDisplayHelper.GetBase64LengthDisplay(Blob); + string mimeInfo = MimeType is not null ? $", MimeType = {MimeType}" : ""; + return $"Uri = \"{Uri}\"{mimeInfo}, Length = {lengthDisplay}"; + } + } } diff --git a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs index 526e5ad..04c98bc 100644 --- a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs +++ b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs @@ -356,6 +356,7 @@ public abstract class ContentBlock } /// Represents text provided to or from an LLM. +[DebuggerDisplay("Text = \"{Text}\"")] public sealed class TextContentBlock : ContentBlock { /// @@ -366,9 +367,13 @@ public sealed class TextContentBlock : ContentBlock /// [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 { /// @@ -388,9 +393,13 @@ public sealed class ImageContentBlock : ContentBlock /// [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 { /// @@ -410,12 +419,16 @@ public sealed class AudioContentBlock : ContentBlock /// [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 { /// @@ -433,12 +446,16 @@ public sealed class EmbeddedResourceBlock : ContentBlock /// [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 { /// @@ -503,6 +520,7 @@ public sealed class ResourceLinkBlock : ContentBlock } /// Represents a request from the assistant to call a tool. +[DebuggerDisplay("Name = {Name}, Id = {Id}")] public sealed class ToolUseContentBlock : ContentBlock { /// @@ -531,6 +549,7 @@ public sealed class ToolUseContentBlock : ContentBlock } /// Represents the result of a tool use, provided by the user back to the assistant. +[DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class ToolResultContentBlock : ContentBlock { /// @@ -575,4 +594,37 @@ public sealed class ToolResultContentBlock : ContentBlock /// [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}"; + } + } } diff --git a/src/ModelContextProtocol.Core/Protocol/DebuggerDisplayHelper.cs b/src/ModelContextProtocol.Core/Protocol/DebuggerDisplayHelper.cs new file mode 100644 index 0000000..77f7193 --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/DebuggerDisplayHelper.cs @@ -0,0 +1,30 @@ +using System; + +namespace ModelContextProtocol.Protocol; + +/// +/// Internal helper methods for DebuggerDisplay implementations. +/// +internal static class DebuggerDisplayHelper +{ + /// + /// Gets the decoded length of base64 data for debugger display. + /// + internal static string GetBase64LengthDisplay(string base64Data) + { +#if NET + if (System.Buffers.Text.Base64.IsValid(base64Data, out int decodedLength)) + { + return $"{decodedLength} bytes"; + } +#else + try + { + return $"{Convert.FromBase64String(base64Data).Length} bytes"; + } + catch { } +#endif + + return "invalid base64"; + } +} diff --git a/src/ModelContextProtocol.Core/Protocol/Icon.cs b/src/ModelContextProtocol.Core/Protocol/Icon.cs index a04c6af..0ae9add 100644 --- a/src/ModelContextProtocol.Core/Protocol/Icon.cs +++ b/src/ModelContextProtocol.Core/Protocol/Icon.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -28,6 +29,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// +[DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class Icon { /// @@ -82,4 +84,14 @@ public sealed class Icon /// [JsonPropertyName("theme")] public string? Theme { get; set; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + { + get + { + string mimeInfo = MimeType is not null ? $", MimeType = {MimeType}" : ""; + return $"Source = \"{Source}\"{mimeInfo}"; + } + } } diff --git a/src/ModelContextProtocol.Core/Protocol/Prompt.cs b/src/ModelContextProtocol.Core/Protocol/Prompt.cs index 43b2bd2..9f5534a 100644 --- a/src/ModelContextProtocol.Core/Protocol/Prompt.cs +++ b/src/ModelContextProtocol.Core/Protocol/Prompt.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using ModelContextProtocol.Server; @@ -10,6 +11,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// +[DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class Prompt : IBaseMetadata { /// @@ -75,4 +77,14 @@ public sealed class Prompt : IBaseMetadata /// [JsonIgnore] public McpServerPrompt? McpServerPrompt { get; set; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + { + get + { + string desc = Description is not null ? $", Description = \"{Description}\"" : ""; + return $"Name = {Name}{desc}"; + } + } } diff --git a/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs b/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs index ca462c3..76dede3 100644 --- a/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs +++ b/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Microsoft.Extensions.AI; using System.Text.Json.Serialization; @@ -26,6 +27,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// +[DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class PromptMessage { /// @@ -49,4 +51,19 @@ public sealed class PromptMessage /// [JsonPropertyName("role")] public required Role Role { get; set; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + { + get + { + // Show actual text content if it's a TextContentBlock + if (Content is TextContentBlock textBlock) + { + return $"Role = {Role}, Text = \"{textBlock.Text}\""; + } + + return $"Role = {Role}, ContentType = {Content.Type}"; + } + } } diff --git a/src/ModelContextProtocol.Core/Protocol/Resource.cs b/src/ModelContextProtocol.Core/Protocol/Resource.cs index 6c0eb91..be2a4aa 100644 --- a/src/ModelContextProtocol.Core/Protocol/Resource.cs +++ b/src/ModelContextProtocol.Core/Protocol/Resource.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -11,6 +12,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// +[DebuggerDisplay("Name = {Name}, Uri = {Uri}")] public sealed class Resource : IBaseMetadata { /// diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs b/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs index 093824e..d929a68 100644 --- a/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs +++ b/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -26,6 +27,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// +[DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class SamplingMessage { /// @@ -49,4 +51,22 @@ public sealed class SamplingMessage /// [JsonPropertyName("_meta")] public JsonObject? Meta { get; set; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + { + get + { + // Show actual text content if it's a single TextContentBlock + if (Content.Count == 1 && Content[0] is TextContentBlock textBlock) + { + return $"Role = {Role}, Text = \"{textBlock.Text}\""; + } + + string contentTypes = Content.Count == 1 + ? Content[0].Type + : $"{Content.Count} items"; + return $"Role = {Role}, Content = {contentTypes}"; + } + } } diff --git a/src/ModelContextProtocol.Core/Protocol/TextResourceContents.cs b/src/ModelContextProtocol.Core/Protocol/TextResourceContents.cs index 9f8a9b8..99c8593 100644 --- a/src/ModelContextProtocol.Core/Protocol/TextResourceContents.cs +++ b/src/ModelContextProtocol.Core/Protocol/TextResourceContents.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -19,6 +20,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for more details. /// /// +[DebuggerDisplay("Uri = \"{Uri}\", Text = \"{Text}\"")] public sealed class TextResourceContents : ResourceContents { /// @@ -26,4 +28,7 @@ public sealed class TextResourceContents : ResourceContents /// [JsonPropertyName("text")] public required string Text { get; set; } + + /// + public override string ToString() => Text ?? ""; } diff --git a/src/ModelContextProtocol.Core/Protocol/Tool.cs b/src/ModelContextProtocol.Core/Protocol/Tool.cs index 9365a85..30b1242 100644 --- a/src/ModelContextProtocol.Core/Protocol/Tool.cs +++ b/src/ModelContextProtocol.Core/Protocol/Tool.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -8,6 +9,7 @@ namespace ModelContextProtocol.Protocol; /// /// Represents a tool that the server is capable of calling. /// +[DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class Tool : IBaseMetadata { /// @@ -130,4 +132,14 @@ public sealed class Tool : IBaseMetadata /// [JsonIgnore] public McpServerTool? McpServerTool { get; set; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + { + get + { + string desc = Description is not null ? $", Description = \"{Description}\"" : ""; + return $"Name = {Name}{desc}"; + } + } }