From a29ddc72a1664df342188f8b1acfe4598ed0d0e5 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 12 Jun 2025 16:52:01 -0400 Subject: [PATCH 1/4] Update content according to latest spec --- .../Tools/AnnotatedMessageTool.cs | 16 +- .../EverythingServer/Tools/SampleLlmTool.cs | 8 +- .../Tools/SampleLlmTool.cs | 8 +- .../CodeAnalysis/RequiresUnreferencedCode.cs | 2 +- .../AIContentExtensions.cs | 79 ++-- .../Client/McpClientExtensions.cs | 58 +-- .../Client/McpClientTool.cs | 18 +- src/ModelContextProtocol.Core/Diagnostics.cs | 2 +- .../McpJsonUtilities.cs | 2 +- .../Protocol/Annotations.cs | 10 + .../Protocol/CallToolRequestParams.cs | 2 +- ...{CallToolResponse.cs => CallToolResult.cs} | 4 +- .../Protocol/Content.cs | 92 ---- .../Protocol/ContentBlock.cs | 395 ++++++++++++++++++ .../Protocol/CreateMessageResult.cs | 2 +- .../Protocol/ElicitResult.cs | 8 +- .../Protocol/PromptMessage.cs | 4 +- .../Protocol/SamplingMessage.cs | 2 +- .../Protocol/Tool.cs | 2 +- .../Protocol/ToolsCapability.cs | 4 +- .../Server/AIFunctionMcpServerPrompt.cs | 2 +- .../Server/AIFunctionMcpServerTool.cs | 26 +- .../Server/DelegatingMcpServerTool.cs | 2 +- .../Server/McpServer.cs | 2 +- .../Server/McpServerExtensions.cs | 27 +- .../Server/McpServerResource.cs | 4 +- .../Server/McpServerResourceAttribute.cs | 4 +- .../Server/McpServerTool.cs | 28 +- .../Server/McpServerToolAttribute.cs | 26 +- .../Server/McpServerToolCreateOptions.cs | 2 +- .../McpServerBuilderExtensions.cs | 2 +- src/ModelContextProtocol/McpServerHandlers.cs | 2 +- tests/Common/Utils/TestServerTransport.cs | 2 +- .../HttpServerIntegrationTests.cs | 19 +- .../MapMcpTests.cs | 21 +- .../SseIntegrationTests.cs | 2 +- .../StatelessServerTests.cs | 8 +- .../StreamableHttpClientConformanceTests.cs | 7 +- .../StreamableHttpServerConformanceTests.cs | 23 +- .../Program.cs | 39 +- .../Program.cs | 39 +- .../Client/McpClientExtensionsTests.cs | 17 +- .../Client/McpClientFactoryTests.cs | 2 +- .../ClientIntegrationTests.cs | 25 +- .../McpServerBuilderExtensionsHandlerTests.cs | 2 +- .../McpServerBuilderExtensionsPromptsTests.cs | 4 +- .../McpServerBuilderExtensionsToolsTests.cs | 33 +- .../DockerEverythingServerTests.cs | 8 +- .../Server/McpServerDelegatesTests.cs | 2 +- .../Server/McpServerPromptTests.cs | 45 +- .../Server/McpServerTests.cs | 14 +- .../Server/McpServerToolTests.cs | 81 ++-- 52 files changed, 726 insertions(+), 512 deletions(-) rename src/ModelContextProtocol.Core/Protocol/{CallToolResponse.cs => CallToolResult.cs} (95%) delete mode 100644 src/ModelContextProtocol.Core/Protocol/Content.cs create mode 100644 src/ModelContextProtocol.Core/Protocol/ContentBlock.cs diff --git a/samples/EverythingServer/Tools/AnnotatedMessageTool.cs b/samples/EverythingServer/Tools/AnnotatedMessageTool.cs index f4c5d257..e6ac535e 100644 --- a/samples/EverythingServer/Tools/AnnotatedMessageTool.cs +++ b/samples/EverythingServer/Tools/AnnotatedMessageTool.cs @@ -15,25 +15,22 @@ public enum MessageType } [McpServerTool(Name = "annotatedMessage"), Description("Generates an annotated message")] - public static IEnumerable AnnotatedMessage(MessageType messageType, bool includeImage = true) + public static IEnumerable AnnotatedMessage(MessageType messageType, bool includeImage = true) { - List contents = messageType switch + List contents = messageType switch { - MessageType.Error => [new() + MessageType.Error => [new TextContentBlock() { - Type = "text", Text = "Error: Operation failed", Annotations = new() { Audience = [Role.User, Role.Assistant], Priority = 1.0f } }], - MessageType.Success => [new() + MessageType.Success => [new TextContentBlock() { - Type = "text", Text = "Operation completed successfully", Annotations = new() { Audience = [Role.User], Priority = 0.7f } }], - MessageType.Debug => [new() + MessageType.Debug => [new TextContentBlock() { - Type = "text", Text = "Debug: Cache hit ratio 0.95, latency 150ms", Annotations = new() { Audience = [Role.Assistant], Priority = 0.3f } }], @@ -42,9 +39,8 @@ public static IEnumerable AnnotatedMessage(MessageType messageType, boo if (includeImage) { - contents.Add(new() + contents.Add(new ImageContentBlock() { - Type = "image", Data = TinyImageTool.MCP_TINY_IMAGE.Split(",").Last(), MimeType = "image/png", Annotations = new() { Audience = [Role.User], Priority = 0.5f } diff --git a/samples/EverythingServer/Tools/SampleLlmTool.cs b/samples/EverythingServer/Tools/SampleLlmTool.cs index 1b49cd29..d2ed9f2e 100644 --- a/samples/EverythingServer/Tools/SampleLlmTool.cs +++ b/samples/EverythingServer/Tools/SampleLlmTool.cs @@ -17,7 +17,7 @@ public static async Task SampleLLM( var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); var sampleResult = await server.SampleAsync(samplingParams, cancellationToken); - return $"LLM sampling result: {sampleResult.Content.Text}"; + return $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}"; } private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100) @@ -27,11 +27,7 @@ private static CreateMessageRequestParams CreateRequestSamplingParams(string con Messages = [new SamplingMessage() { Role = Role.User, - Content = new Content() - { - Type = "text", - Text = $"Resource {uri} context: {context}" - } + Content = new TextContentBlock() { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs index 964f7b31..67cd33b3 100644 --- a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs +++ b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs @@ -20,7 +20,7 @@ public static async Task SampleLLM( var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken); - return $"LLM sampling result: {sampleResult.Content.Text}"; + return $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}"; } private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100) @@ -30,11 +30,7 @@ private static CreateMessageRequestParams CreateRequestSamplingParams(string con Messages = [new SamplingMessage() { Role = Role.User, - Content = new Content() - { - Type = "text", - Text = $"Resource {uri} context: {context}" - } + Content = new TextContentBlock() { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/RequiresUnreferencedCode.cs b/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/RequiresUnreferencedCode.cs index e4a5344e..3e845a53 100644 --- a/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/RequiresUnreferencedCode.cs +++ b/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/RequiresUnreferencedCode.cs @@ -5,7 +5,7 @@ namespace System.Diagnostics.CodeAnalysis; /// /// Indicates that the specified method requires dynamic access to code that is not referenced -/// statically, for example through . +/// statically, for example through . /// /// /// This allows tools to understand which methods are unsafe to call when removing unreferenced diff --git a/src/ModelContextProtocol.Core/AIContentExtensions.cs b/src/ModelContextProtocol.Core/AIContentExtensions.cs index bbdf4d54..762e0706 100644 --- a/src/ModelContextProtocol.Core/AIContentExtensions.cs +++ b/src/ModelContextProtocol.Core/AIContentExtensions.cs @@ -29,11 +29,13 @@ public static ChatMessage ToChatMessage(this PromptMessage promptMessage) { Throw.IfNull(promptMessage); + AIContent? content = ToAIContent(promptMessage.Content); + return new() { RawRepresentation = promptMessage, Role = promptMessage.Role == Role.User ? ChatRole.User : ChatRole.Assistant, - Contents = [ToAIContent(promptMessage.Content)] + Contents = content is not null ? [content] : [], }; } @@ -81,33 +83,33 @@ public static IList ToPromptMessages(this ChatMessage chatMessage return messages; } - /// Creates a new from the content of a . - /// The to convert. - /// The created . + /// Creates a new from the content of a . + /// The to convert. + /// + /// The created . If the content can't be converted (such as when it's a resource link), is returned. + /// /// /// This method converts Model Context Protocol content types to the equivalent Microsoft.Extensions.AI /// content types, enabling seamless integration between the protocol and AI client libraries. /// - public static AIContent ToAIContent(this Content content) + public static AIContent? ToAIContent(this ContentBlock content) { Throw.IfNull(content); - AIContent ac; - if (content is { Type: "image" or "audio", MimeType: not null, Data: not null }) + AIContent? ac = content switch { - ac = new DataContent(Convert.FromBase64String(content.Data), content.MimeType); - } - else if (content is { Type: "resource" } && content.Resource is { } resourceContents) - { - ac = resourceContents.ToAIContent(); - } - else + TextContentBlock textContent => new TextContent(textContent.Text), + ImageContentBlock imageContent => new DataContent(Convert.FromBase64String(imageContent.Data), imageContent.MimeType), + AudioContentBlock audioContent => new DataContent(Convert.FromBase64String(audioContent.Data), audioContent.MimeType), + EmbeddedResourceBlock resourceContent => resourceContent.Resource.ToAIContent(), + _ => null, + }; + + if (ac is not null) { - ac = new TextContent(content.Text); + ac.RawRepresentation = content; } - ac.RawRepresentation = content; - return ac; } @@ -135,8 +137,8 @@ public static AIContent ToAIContent(this ResourceContents content) return ac; } - /// Creates a list of from a sequence of . - /// The instances to convert. + /// Creates a list of from a sequence of . + /// The instances to convert. /// The created instances. /// /// @@ -145,15 +147,15 @@ public static AIContent ToAIContent(this ResourceContents content) /// when processing the contents of a message or response. /// /// - /// Each object is converted using , + /// Each object is converted using , /// preserving the type-specific conversion logic for text, images, audio, and resources. /// /// - public static IList ToAIContents(this IEnumerable contents) + public static IList ToAIContents(this IEnumerable contents) { Throw.IfNull(contents); - return [.. contents.Select(ToAIContent)]; + return [.. contents.Select(ToAIContent).OfType()]; } /// Creates a list of from a sequence of . @@ -167,7 +169,7 @@ public static IList ToAIContents(this IEnumerable contents) /// /// /// Each object is converted using , - /// preserving the type-specific conversion logic: text resources become objects and + /// preserving the type-specific conversion logic: text resources become objects and /// binary resources become objects. /// /// @@ -178,29 +180,38 @@ public static IList ToAIContents(this IEnumerable c return [.. contents.Select(ToAIContent)]; } - internal static Content ToContent(this AIContent content) => + internal static ContentBlock ToContent(this AIContent content) => content switch { - TextContent textContent => new() + TextContent textContent => new TextContentBlock() { Text = textContent.Text, - Type = "text", }, - DataContent dataContent => new() + DataContent dataContent when dataContent.HasTopLevelMediaType("image") => new ImageContentBlock() { Data = dataContent.Base64Data.ToString(), MimeType = dataContent.MediaType, - Type = - dataContent.HasTopLevelMediaType("image") ? "image" : - dataContent.HasTopLevelMediaType("audio") ? "audio" : - "resource", }, - - _ => new() + + DataContent dataContent when dataContent.HasTopLevelMediaType("audio") => new AudioContentBlock() + { + Data = dataContent.Base64Data.ToString(), + MimeType = dataContent.MediaType, + }, + + DataContent dataContent => new EmbeddedResourceBlock() + { + Resource = new BlobResourceContents() + { + Blob = dataContent.Base64Data.ToString(), + MimeType = dataContent.MediaType, + } + }, + + _ => new TextContentBlock() { Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))), - Type = "text", } }; } diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index 59aac7fb..803388c4 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -812,7 +812,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, /// /// The to monitor for cancellation requests. The default is . /// - /// A task containing the from the tool execution. The response includes + /// A task containing the from the tool execution. The response includes /// the tool's output content, which may be structured data, text, or an error message. /// /// is . @@ -829,7 +829,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, /// }); /// /// - public static ValueTask CallToolAsync( + public static ValueTask CallToolAsync( this IMcpClient client, string toolName, IReadOnlyDictionary? arguments = null, @@ -855,10 +855,10 @@ public static ValueTask CallToolAsync( Arguments = ToArgumentsDictionary(arguments, serializerOptions), }, McpJsonUtilities.JsonContext.Default.CallToolRequestParams, - McpJsonUtilities.JsonContext.Default.CallToolResponse, + McpJsonUtilities.JsonContext.Default.CallToolResult, cancellationToken: cancellationToken); - static async ValueTask SendRequestWithProgressAsync( + static async ValueTask SendRequestWithProgressAsync( IMcpClient client, string toolName, IReadOnlyDictionary? arguments, @@ -889,7 +889,7 @@ static async ValueTask SendRequestWithProgressAsync( Meta = new() { ProgressToken = progressToken }, }, McpJsonUtilities.JsonContext.Default.CallToolRequestParams, - McpJsonUtilities.JsonContext.Default.CallToolResponse, + McpJsonUtilities.JsonContext.Default.CallToolResult, cancellationToken: cancellationToken).ConfigureAwait(false); } } @@ -924,29 +924,12 @@ internal static (IList Messages, ChatOptions? Options) ToChatClient (options ??= new()).StopSequences = stopSequences.ToArray(); } - List messages = []; - foreach (SamplingMessage sm in requestParams.Messages) - { - ChatMessage message = new() - { - Role = sm.Role == Role.User ? ChatRole.User : ChatRole.Assistant, - }; - - if (sm.Content is { Type: "text" }) - { - message.Contents.Add(new TextContent(sm.Content.Text)); - } - else if (sm.Content is { Type: "image" or "audio", MimeType: not null, Data: not null }) - { - message.Contents.Add(new DataContent(Convert.FromBase64String(sm.Content.Data), sm.Content.MimeType)); - } - else if (sm.Content is { Type: "resource", Resource: not null }) - { - message.Contents.Add(sm.Content.Resource.ToAIContent()); - } - - messages.Add(message); - } + List messages = + (from sm in requestParams.Messages + let aiContent = sm.Content.ToAIContent() + where aiContent is not null + select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent])) + .ToList(); return (messages, options); } @@ -965,32 +948,21 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault(); - Content? content = null; + ContentBlock? content = null; if (lastMessage is not null) { foreach (var lmc in lastMessage.Contents) { if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio"))) { - content = new() - { - Type = dc.HasTopLevelMediaType("image") ? "image" : "audio", - MimeType = dc.MediaType, - Data = dc.Base64Data.ToString(), - }; + content = dc.ToContent(); } } } - content ??= new() - { - Text = lastMessage?.Text ?? string.Empty, - Type = "text", - }; - return new() { - Content = content, + Content = content ?? new TextContentBlock() { Text = lastMessage?.Text ?? string.Empty }, Model = chatResponse.ModelId ?? "unknown", Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", @@ -1107,7 +1079,7 @@ public static Task SetLoggingLevel(this IMcpClient client, LogLevel level, Cance SetLoggingLevel(client, McpServer.ToLoggingLevel(level), cancellationToken); /// Convers a dictionary with values to a dictionary with values. - private static IReadOnlyDictionary? ToArgumentsDictionary( + private static Dictionary? ToArgumentsDictionary( IReadOnlyDictionary? arguments, JsonSerializerOptions options) { var typeInfo = options.GetTypeInfo(); diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs index 11d72a86..e43d46da 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs @@ -84,8 +84,8 @@ internal McpClientTool( protected async override ValueTask InvokeCoreAsync( AIFunctionArguments arguments, CancellationToken cancellationToken) { - CallToolResponse result = await CallAsync(arguments, _progress, JsonSerializerOptions, cancellationToken).ConfigureAwait(false); - return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse); + CallToolResult result = await CallAsync(arguments, _progress, JsonSerializerOptions, cancellationToken).ConfigureAwait(false); + return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResult); } /// @@ -104,14 +104,14 @@ internal McpClientTool( /// /// The to monitor for cancellation requests. The default is . /// - /// A task containing the from the tool execution. The response includes + /// A task containing the from the tool execution. The response includes /// the tool's output content, which may be structured data, text, or an error message. /// /// /// The base method is overridden to invoke this method. - /// The only difference in behavior is will serialize the resulting "/> - /// such that the returned is a containing the serialized . - /// This method is intended to be called directly by user code, whereas the base + /// The only difference in behavior is will serialize the resulting "/> + /// such that the returned is a containing the serialized . + /// This method is intended to be called directly by user code, whereas the base /// is intended to be used polymorphically via the base class, typically as part of an operation. /// /// The server could not find the requested tool, or the server encountered an error while processing the request. @@ -124,7 +124,7 @@ internal McpClientTool( /// }); /// /// - public ValueTask CallAsync( + public ValueTask CallAsync( IReadOnlyDictionary? arguments = null, IProgress? progress = null, JsonSerializerOptions? serializerOptions = null, @@ -156,7 +156,7 @@ public ValueTask CallAsync( /// /// public McpClientTool WithName(string name) => - new McpClientTool(_client, ProtocolTool, JsonSerializerOptions, name, _description, _progress); + new(_client, ProtocolTool, JsonSerializerOptions, name, _description, _progress); /// /// Creates a new instance of the tool but modified to return the specified description from its property. @@ -180,7 +180,7 @@ public McpClientTool WithName(string name) => /// /// A new instance of with the provided description. public McpClientTool WithDescription(string description) => - new McpClientTool(_client, ProtocolTool, JsonSerializerOptions, _name, description, _progress); + new(_client, ProtocolTool, JsonSerializerOptions, _name, description, _progress); /// /// Creates a new instance of the tool but modified to report progress via the specified . diff --git a/src/ModelContextProtocol.Core/Diagnostics.cs b/src/ModelContextProtocol.Core/Diagnostics.cs index 9ee2d06a..308d07c5 100644 --- a/src/ModelContextProtocol.Core/Diagnostics.cs +++ b/src/ModelContextProtocol.Core/Diagnostics.cs @@ -13,7 +13,7 @@ internal static class Diagnostics internal static Meter Meter { get; } = new("Experimental.ModelContextProtocol"); internal static Histogram CreateDurationHistogram(string name, string description, bool longBuckets) => - Diagnostics.Meter.CreateHistogram(name, "s", description + Meter.CreateHistogram(name, "s", description #if NET9_0_OR_GREATER , advice: longBuckets ? LongSecondsBucketBoundaries : ShortSecondsBucketBoundaries #endif diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs index 1f0a3089..0ce13888 100644 --- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs +++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs @@ -100,7 +100,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element) // MCP Request Params / Results [JsonSerializable(typeof(CallToolRequestParams))] - [JsonSerializable(typeof(CallToolResponse))] + [JsonSerializable(typeof(CallToolResult))] [JsonSerializable(typeof(CancelledNotification))] [JsonSerializable(typeof(CompleteRequestParams))] [JsonSerializable(typeof(CompleteResult))] diff --git a/src/ModelContextProtocol.Core/Protocol/Annotations.cs b/src/ModelContextProtocol.Core/Protocol/Annotations.cs index e937607c..f9aa58a5 100644 --- a/src/ModelContextProtocol.Core/Protocol/Annotations.cs +++ b/src/ModelContextProtocol.Core/Protocol/Annotations.cs @@ -26,4 +26,14 @@ public class Annotations /// [JsonPropertyName("priority")] public float? Priority { get; init; } + + /// + /// Gets or sets the moment the resource was last modified, as an ISO 8601 formatted string. + /// + /// + /// Should be an ISO 8601 formatted string (e.g., \"2025-01-12T15:00:58Z\"). + /// Examples: last activity timestamp in an open file, timestamp when the resource was attached, etc. + /// + [JsonPropertyName("lastModified")] + public string? LastModified { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs index 81e8d4f4..fbe4d3ff 100644 --- a/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs @@ -7,7 +7,7 @@ namespace ModelContextProtocol.Protocol; /// Represents the parameters used with a request from a client to invoke a tool provided by the server. /// /// -/// The server will respond with a containing the result of the tool invocation. +/// The server will respond with a containing the result of the tool invocation. /// See the schema for details. /// public class CallToolRequestParams : RequestParams diff --git a/src/ModelContextProtocol.Core/Protocol/CallToolResponse.cs b/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs similarity index 95% rename from src/ModelContextProtocol.Core/Protocol/CallToolResponse.cs rename to src/ModelContextProtocol.Core/Protocol/CallToolResult.cs index 173041f4..b23d4081 100644 --- a/src/ModelContextProtocol.Core/Protocol/CallToolResponse.cs +++ b/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs @@ -20,13 +20,13 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class CallToolResponse +public class CallToolResult { /// /// Gets or sets the response content from the tool call. /// [JsonPropertyName("content")] - public List Content { get; set; } = []; + public List Content { get; set; } = []; /// /// Gets or sets an optional JSON object representing the structured result of the tool call. diff --git a/src/ModelContextProtocol.Core/Protocol/Content.cs b/src/ModelContextProtocol.Core/Protocol/Content.cs deleted file mode 100644 index d0cdaaa8..00000000 --- a/src/ModelContextProtocol.Core/Protocol/Content.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Text.Json.Serialization; - -namespace ModelContextProtocol.Protocol; - -/// -/// Represents content within the Model Context Protocol (MCP) that can contain text, binary data, or references to resources. -/// -/// -/// -/// The class is a fundamental type in the MCP that can represent different forms of content -/// based on the property. The main content types are: -/// -/// -/// "text" - Textual content, stored in the property -/// "image" - Image data, stored as base64 in the property with appropriate MIME type -/// "audio" - Audio data, stored as base64 in the property with appropriate MIME type -/// "resource" - Reference to a resource, accessed through the property -/// -/// -/// This class is used extensively throughout the MCP for representing content in messages, tool responses, -/// and other communication between clients and servers. -/// -/// -/// See the schema for more details. -/// -/// -public class Content -{ - /// - /// Gets or sets the type of content. - /// - /// - /// This determines the structure of the content object. Valid values include "image", "audio", "text", and "resource". - /// - [JsonPropertyName("type")] - public string Type { get; set; } = "text"; - - /// - /// Gets or sets the text content of the message. - /// - [JsonPropertyName("text")] - public string? Text { get; set; } - - /// - /// Gets or sets the base64-encoded image or audio data. - /// - [JsonPropertyName("data")] - public string? Data { get; set; } - - /// - /// Gets or sets the MIME type (or "media type") of the content, specifying the format of the data. - /// - /// - /// - /// This property is used when is "image", "audio", or "resource", to indicate the specific format of the binary data. - /// Common values include "image/png", "image/jpeg", "audio/wav", and "audio/mp3". - /// - /// - /// This property is required when the property contains binary content, - /// as it helps clients properly interpret and render the content. - /// - /// - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } - - /// - /// Gets or sets the resource content of the message when is "resource". - /// - /// - /// - /// This property is used to embed or reference resource data within a message. It's only - /// applicable when the property is set to "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 ResourceContents? Resource { get; set; } - - /// - /// 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; init; } -} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs new file mode 100644 index 00000000..6e5ad1d9 --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs @@ -0,0 +1,395 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Text.Json; +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() + { + } + + /// + /// Gets or sets the type of content. + /// + /// + /// This determines the structure of the content object. Valid values include "image", "audio", "text", "resource", and "resource_link". + /// + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + /// + /// 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; init; } + + /// + /// Provides a for . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public 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? data = null; + string? mimeType = null; + string? uri = null; + string? description = null; + long? size = null; + ResourceContents? resource = null; + Annotations? annotations = 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 "data": + data = reader.GetString(); + 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 "resource": + resource = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.ResourceContents); + break; + + case "annotations": + annotations = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.Annotations); + break; + + default: + break; + } + } + + switch (type) + { + case "text": + return new TextContentBlock() + { + Text = text ?? throw new JsonException("Text contents must be provided for 'text' type."), + Annotations = annotations, + }; + + case "image": + return 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."), + Annotations = annotations, + }; + + case "audio": + return 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."), + Annotations = annotations, + }; + + case "resource": + return new EmbeddedResourceBlock() + { + Resource = resource ?? throw new JsonException("Resource contents must be provided for 'resource' type."), + Annotations = annotations + }; + + case "resource_link": + return 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."), + Description = description, + MimeType = mimeType, + Size = size, + Annotations = annotations, + }; + + default: + throw new JsonException($"Unknown content type: '{type}'"); + } + } + + /// + 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); + writer.WriteString("mimeType", imageContent.MimeType); + break; + + case AudioContentBlock audioContent: + writer.WriteString("data", audioContent.Data); + 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.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); + } + break; + } + + if (value.Annotations is { } annotations) + { + writer.WritePropertyName("annotations"); + JsonSerializer.Serialize(writer, annotations, McpJsonUtilities.JsonContext.Default.Annotations); + } + + writer.WriteEndObject(); + } + } +} + +/// Represents text provided to or from an LLM. +public sealed class TextContentBlock : ContentBlock +{ + /// Initializes the instance of the class. + public TextContentBlock() => Type = "text"; + + /// + /// Gets or sets the text content of the message. + /// + [JsonPropertyName("text")] + public required string Text { get; set; } +} + +/// Represents an image provided to or from an LLM. +public sealed class ImageContentBlock : ContentBlock +{ + /// Initializes the instance of the class. + public ImageContentBlock() => Type = "image"; + + /// + /// Gets or sets the base64-encoded image data. + /// + [JsonPropertyName("data")] + public required string Data { get; set; } + + /// + /// 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; } +} + +/// Represents audio provided to or from an LLM. +public sealed class AudioContentBlock : ContentBlock +{ + /// Initializes the instance of the class. + public AudioContentBlock() => Type = "audio"; + + /// + /// Gets or sets the base64-encoded audio data. + /// + [JsonPropertyName("data")] + public required string Data { get; set; } + + /// + /// 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; } +} + +/// 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. +/// +public sealed class EmbeddedResourceBlock : ContentBlock +{ + /// Initializes the instance of the class. + public EmbeddedResourceBlock() => 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; } +} + +/// 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. +/// +public sealed class ResourceLinkBlock : ContentBlock +{ + /// Initializes the instance of the class. + public ResourceLinkBlock() => Type = "resource_link"; + + /// + /// Gets or sets the URI of this resource. + /// + [JsonPropertyName("uri")] + public required string Uri { get; init; } + + /// + /// Gets or sets a human-readable name for this resource. + /// + [JsonPropertyName("name")] + public required string Name { get; init; } + + /// + /// Gets or sets a description of what this resource represents. + /// + /// + /// + /// This 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; init; } + + /// + /// 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 may be if the MIME type is unknown or not applicable for the resource. + /// + /// + [JsonPropertyName("mimeType")] + public string? MimeType { get; init; } + + /// + /// Gets or sets the size of the raw resource content (before base64 encoding), in bytes, if known. + /// + /// + /// This can be used by applications to display file sizes and estimate context window usage. + /// + [JsonPropertyName("size")] + public long? Size { get; init; } +} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs index 190c1dd5..b18e0796 100644 --- a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs @@ -14,7 +14,7 @@ public class CreateMessageResult /// Gets or sets the content of the message. /// [JsonPropertyName("content")] - public required Content Content { get; init; } + public required ContentBlock Content { get; init; } /// /// Gets or sets the name of the model that generated the message. diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs index e45e887f..4de7ded0 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs @@ -34,8 +34,14 @@ public class ElicitResult /// Gets or sets the submitted form data. /// /// + /// /// This is typically omitted if the action is "cancel" or "decline". + /// + /// + /// Values in the dictionary should be of types , , + /// , or . + /// /// [JsonPropertyName("content")] - public JsonElement? Content { get; set; } + public IDictionary? Content { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs b/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs index c2dfd152..65418a67 100644 --- a/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs +++ b/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs @@ -34,10 +34,10 @@ public class PromptMessage /// /// The object contains all the message payload, whether it's simple text, /// base64-encoded binary data (for images/audio), or a reference to an embedded resource. - /// The property indicates the specific content type. + /// The property indicates the specific content type. /// [JsonPropertyName("content")] - public Content Content { get; set; } = new(); + public ContentBlock Content { get; set; } = new TextContentBlock() { Text = "" }; /// /// Gets or sets the role of the message sender, specifying whether it's from a "user" or an "assistant". diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs b/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs index 0cbbb1c3..ca50b74c 100644 --- a/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs +++ b/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs @@ -29,7 +29,7 @@ public class SamplingMessage /// Gets or sets the content of the message. /// [JsonPropertyName("content")] - public required Content Content { get; init; } + public required ContentBlock Content { get; init; } /// /// Gets or sets the role of the message sender, indicating whether it's from a "user" or an "assistant". diff --git a/src/ModelContextProtocol.Core/Protocol/Tool.cs b/src/ModelContextProtocol.Core/Protocol/Tool.cs index 8ebf0f9b..552f2538 100644 --- a/src/ModelContextProtocol.Core/Protocol/Tool.cs +++ b/src/ModelContextProtocol.Core/Protocol/Tool.cs @@ -74,7 +74,7 @@ public JsonElement InputSchema /// if an invalid schema is provided. /// /// - /// The schema should describe the shape of the data as returned in . + /// The schema should describe the shape of the data as returned in . /// /// [JsonPropertyName("outputSchema")] diff --git a/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs b/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs index b5789de7..d6f474ea 100644 --- a/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs @@ -42,10 +42,10 @@ public class ToolsCapability /// This handler is invoked when a client makes a call to a tool that isn't found in the . /// The handler should implement logic to execute the requested tool and return appropriate results. /// It receives a containing information about the tool - /// being called and its arguments, and should return a with the execution results. + /// being called and its arguments, and should return a with the execution results. /// [JsonIgnore] - public Func, CancellationToken, ValueTask>? CallToolHandler { get; set; } + public Func, CancellationToken, ValueTask>? CallToolHandler { get; set; } /// /// Gets or sets a collection of tools served by the server. diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs index 479a0004..9d18f3c2 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs @@ -248,7 +248,7 @@ public override async ValueTask GetAsync( string text => new() { Description = ProtocolPrompt.Description, - Messages = [new() { Role = Role.User, Content = new() { Text = text, Type = "text" } }], + Messages = [new() { Role = Role.User, Content = new TextContentBlock() { Text = text } }], }, PromptMessage promptMessage => new() diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index cbb8d973..2146504b 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -260,7 +260,7 @@ private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider public override Tool ProtocolTool { get; } /// - public override async ValueTask InvokeAsync( + public override async ValueTask InvokeAsync( RequestContext request, CancellationToken cancellationToken = default) { Throw.IfNull(request); @@ -297,7 +297,7 @@ public override async ValueTask InvokeAsync( return new() { IsError = true, - Content = [new() { Text = errorMessage, Type = "text" }], + Content = [new TextContentBlock() { Text = errorMessage }], }; } @@ -319,11 +319,11 @@ public override async ValueTask InvokeAsync( string text => new() { - Content = [new() { Text = text, Type = "text" }], + Content = [new TextContentBlock() { Text = text }], StructuredContent = structuredContent, }, - Content content => new() + ContentBlock content => new() { Content = [content], StructuredContent = structuredContent, @@ -331,27 +331,23 @@ public override async ValueTask InvokeAsync( IEnumerable texts => new() { - Content = [.. texts.Select(x => new Content() { Type = "text", Text = x ?? string.Empty })], + Content = [.. texts.Select(x => new TextContentBlock() { Text = x ?? string.Empty })], StructuredContent = structuredContent, }, - IEnumerable contentItems => ConvertAIContentEnumerableToCallToolResponse(contentItems, structuredContent), + IEnumerable contentItems => ConvertAIContentEnumerableToCallToolResult(contentItems, structuredContent), - IEnumerable contents => new() + IEnumerable contents => new() { Content = [.. contents], StructuredContent = structuredContent, }, - CallToolResponse callToolResponse => callToolResponse, + CallToolResult callToolResponse => callToolResponse, _ => new() { - Content = [new() - { - Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))), - Type = "text" - }], + Content = [new TextContentBlock() { Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))) }], StructuredContent = structuredContent, }, }; @@ -434,9 +430,9 @@ typeProperty.ValueKind is not JsonValueKind.String || return nodeResult; } - private static CallToolResponse ConvertAIContentEnumerableToCallToolResponse(IEnumerable contentItems, JsonNode? structuredContent) + private static CallToolResult ConvertAIContentEnumerableToCallToolResult(IEnumerable contentItems, JsonNode? structuredContent) { - List contentList = []; + List contentList = []; bool allErrorContent = true; bool hasAny = false; diff --git a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs index a1f4be1d..6cad5e79 100644 --- a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs @@ -23,7 +23,7 @@ protected DelegatingMcpServerTool(McpServerTool innerTool) public override Tool ProtocolTool => _innerTool.ProtocolTool; /// - public override ValueTask InvokeAsync( + public override ValueTask InvokeAsync( RequestContext request, CancellationToken cancellationToken = default) => _innerTool.InvokeAsync(request, cancellationToken); diff --git a/src/ModelContextProtocol.Core/Server/McpServer.cs b/src/ModelContextProtocol.Core/Server/McpServer.cs index b715edda..81473da0 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.cs @@ -467,7 +467,7 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false) RequestMethods.ToolsCall, callToolHandler, McpJsonUtilities.JsonContext.Default.CallToolRequestParams, - McpJsonUtilities.JsonContext.Default.CallToolResponse); + McpJsonUtilities.JsonContext.Default.CallToolResult); } private void ConfigureLogging(McpServerOptions options) diff --git a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs index bdd6eb18..6178d5a9 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs @@ -94,11 +94,7 @@ public static async Task SampleAsync( samplingMessages.Add(new() { Role = role, - Content = new() - { - Type = "text", - Text = textContent.Text, - }, + Content = new TextContentBlock() { Text = textContent.Text }, }); break; @@ -106,12 +102,17 @@ public static async Task SampleAsync( samplingMessages.Add(new() { Role = role, - Content = new() - { - Type = dataContent.HasTopLevelMediaType("image") ? "image" : "audio", - MimeType = dataContent.MediaType, - Data = dataContent.Base64Data.ToString(), - }, + Content = dataContent.HasTopLevelMediaType("image") ? + new ImageContentBlock() + { + MimeType = dataContent.MediaType, + Data = dataContent.Base64Data.ToString(), + } : + new AudioContentBlock() + { + MimeType = dataContent.MediaType, + Data = dataContent.Base64Data.ToString(), + }, }); break; } @@ -135,7 +136,9 @@ public static async Task SampleAsync( ModelPreferences = modelPreferences, }, cancellationToken).ConfigureAwait(false); - return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, [result.Content.ToAIContent()])) + AIContent? responseContent = result.Content.ToAIContent(); + + return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContent is not null ? [responseContent] : [])) { ModelId = result.Model, FinishReason = result.StopReason switch diff --git a/src/ModelContextProtocol.Core/Server/McpServerResource.cs b/src/ModelContextProtocol.Core/Server/McpServerResource.cs index e85ec732..c7ae8fda 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResource.cs @@ -90,7 +90,7 @@ namespace ModelContextProtocol.Server; /// Wrapped in a list containing the single . /// /// -/// +/// /// Converted to a list containing a single . /// /// @@ -107,7 +107,7 @@ namespace ModelContextProtocol.Server; /// /// /// of -/// Converted to a list containing a for each and a for each . +/// Converted to a list containing a for each and a for each . /// /// /// of diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs index 5cc7a4ef..9bdfa4d0 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs @@ -78,7 +78,7 @@ namespace ModelContextProtocol.Server; /// Wrapped in a list containing the single . /// /// -/// +/// /// Converted to a list containing a single . /// /// @@ -95,7 +95,7 @@ namespace ModelContextProtocol.Server; /// /// /// of -/// Converted to a list containing a for each and a for each . +/// Converted to a list containing a for each and a for each . /// /// /// of diff --git a/src/ModelContextProtocol.Core/Server/McpServerTool.cs b/src/ModelContextProtocol.Core/Server/McpServerTool.cs index a4b0cc58..95b9d0f4 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerTool.cs @@ -24,7 +24,7 @@ namespace ModelContextProtocol.Server; /// , and are what are used implicitly by WithToolsFromAssembly and WithTools. The methods /// create instances capable of working with a large variety of .NET method signatures, automatically handling /// how parameters are marshaled into the method from the JSON received from the MCP client, and how the return value is marshaled back -/// into the that's then serialized and sent back to the client. +/// into the that's then serialized and sent back to the client. /// /// /// By default, parameters are sourced from the dictionary, which is a collection @@ -90,44 +90,44 @@ namespace ModelContextProtocol.Server; /// to provide data to the method. /// /// -/// Return values from a method are used to create the that is sent back to the client: +/// Return values from a method are used to create the that is sent back to the client: /// /// /// /// -/// Returns an empty list. +/// Returns an empty list. /// /// /// -/// Converted to a single object using . +/// Converted to a single object using . /// /// /// -/// Converted to a single object with set to the string value and set to "text". +/// Converted to a single object with its text set to the string value. /// /// -/// -/// Returned as a single-item list. +/// +/// Returned as a single-item list. /// /// /// of -/// Each is converted to a object with set to the string value and set to "text". +/// Each is converted to a object with its text set to the string value. /// /// /// of -/// Each is converted to a object using . +/// Each is converted to a object using . /// /// -/// of -/// Returned as the list. +/// of +/// Returned as the list. /// /// -/// +/// /// Returned directly without modification. /// /// /// Other types -/// Serialized to JSON and returned as a single object with set to "text". +/// Serialized to JSON and returned as a single object with set to "text". /// /// /// @@ -146,7 +146,7 @@ protected McpServerTool() /// The to monitor for cancellation requests. The default is . /// The call response from invoking the tool. /// is . - public abstract ValueTask InvokeAsync( + public abstract ValueTask InvokeAsync( RequestContext request, CancellationToken cancellationToken = default); diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs index 95556174..247516f0 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs @@ -81,44 +81,44 @@ namespace ModelContextProtocol.Server; /// to provide data to the method. /// /// -/// Return values from a method are used to create the that is sent back to the client: +/// Return values from a method are used to create the that is sent back to the client: /// /// /// /// -/// Returns an empty list. +/// Returns an empty list. /// /// /// -/// Converted to a single object using . +/// Converted to a single object using . /// /// /// -/// Converted to a single object with set to the string value and set to "text". +/// Converted to a single object with its text set to the string value. /// /// -/// -/// Returned as a single-item list. +/// +/// Returned as a single-item list. /// /// /// of -/// Each is converted to a object with set to the string value and set to "text". +/// Each is converted to a object with its text set to the string value. /// /// /// of -/// Each is converted to a object using . +/// Each is converted to a object using . /// /// -/// of -/// Returned as the list. +/// of +/// Returned as the list. /// /// -/// +/// /// Returned directly without modification. /// /// /// Other types -/// Serialized to JSON and returned as a single object with set to "text". +/// Serialized to JSON and returned as a single object with set to "text". /// /// /// @@ -246,7 +246,7 @@ public bool ReadOnly /// /// /// When enabled, the tool will attempt to populate the - /// and provide structured content in the property. + /// and provide structured content in the property. /// public bool UseStructuredContent { get; set; } } diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs index 67ffe88f..63407d88 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs @@ -130,7 +130,7 @@ public sealed class McpServerToolCreateOptions /// /// /// When enabled, the tool will attempt to populate the - /// and provide structured content in the property. + /// and provide structured content in the property. /// public bool UseStructuredContent { get; set; } diff --git a/src/ModelContextProtocol/McpServerBuilderExtensions.cs b/src/ModelContextProtocol/McpServerBuilderExtensions.cs index e7a266d0..d925b24f 100644 --- a/src/ModelContextProtocol/McpServerBuilderExtensions.cs +++ b/src/ModelContextProtocol/McpServerBuilderExtensions.cs @@ -504,7 +504,7 @@ public static IMcpServerBuilder WithListToolsHandler(this IMcpServerBuilder buil /// This method is typically paired with to provide a complete tools implementation, /// where advertises available tools and this handler executes them. /// - public static IMcpServerBuilder WithCallToolHandler(this IMcpServerBuilder builder, Func, CancellationToken, ValueTask> handler) + public static IMcpServerBuilder WithCallToolHandler(this IMcpServerBuilder builder, Func, CancellationToken, ValueTask> handler) { Throw.IfNull(builder); diff --git a/src/ModelContextProtocol/McpServerHandlers.cs b/src/ModelContextProtocol/McpServerHandlers.cs index 83b7ad90..a07c81b5 100644 --- a/src/ModelContextProtocol/McpServerHandlers.cs +++ b/src/ModelContextProtocol/McpServerHandlers.cs @@ -49,7 +49,7 @@ public sealed class McpServerHandlers /// This handler is invoked when a client makes a call to a tool that isn't found in the collection. /// The handler should implement logic to execute the requested tool and return appropriate results. /// - public Func, CancellationToken, ValueTask>? CallToolHandler { get; set; } + public Func, CancellationToken, ValueTask>? CallToolHandler { get; set; } /// /// Gets or sets the handler for requests. diff --git a/tests/Common/Utils/TestServerTransport.cs b/tests/Common/Utils/TestServerTransport.cs index 1679fee6..70b77f44 100644 --- a/tests/Common/Utils/TestServerTransport.cs +++ b/tests/Common/Utils/TestServerTransport.cs @@ -74,7 +74,7 @@ private async Task SamplingAsync(JsonRpcRequest request, CancellationToken cance await WriteMessageAsync(new JsonRpcResponse { Id = request.Id, - Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new(), Model = "model", Role = Role.User }, McpJsonUtilities.DefaultOptions), + Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new TextContentBlock() { Text = "" }, Model = "model", Role = Role.User }, McpJsonUtilities.DefaultOptions), }, cancellationToken); } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index 8fc7fb3d..394fa497 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -95,7 +95,7 @@ public async Task CallTool_Sse_EchoServer() // assert Assert.NotNull(result); Assert.False(result.IsError); - var textContent = Assert.Single(result.Content, c => c.Type == "text"); + var textContent = Assert.Single(result.Content.OfType()); Assert.Equal("Echo: Hello MCP!", textContent.Text); } @@ -119,9 +119,9 @@ public async Task CallTool_EchoSessionId_ReturnsTheSameSessionId() Assert.False(result2.IsError); Assert.False(result3.IsError); - var textContent1 = Assert.Single(result1.Content); - var textContent2 = Assert.Single(result2.Content); - var textContent3 = Assert.Single(result3.Content); + var textContent1 = Assert.Single(result1.Content.OfType()); + var textContent2 = Assert.Single(result2.Content.OfType()); + var textContent3 = Assert.Single(result3.Content.OfType()); Assert.NotNull(textContent1.Text); Assert.Equal(textContent1.Text, textContent2.Text); @@ -259,11 +259,7 @@ public async Task Sampling_Sse_TestServer() { Model = "test-model", Role = Role.Assistant, - Content = new Content - { - Type = "text", - Text = "Test response" - } + Content = new TextContentBlock { Text = "Test response" }, }; }; await using var client = await GetClientAsync(options); @@ -279,8 +275,7 @@ public async Task Sampling_Sse_TestServer() // assert Assert.NotNull(result); - var textContent = Assert.Single(result.Content); - Assert.Equal("text", textContent.Type); + var textContent = Assert.Single(result.Content.OfType()); Assert.False(string.IsNullOrEmpty(textContent.Text)); } @@ -304,7 +299,7 @@ public async Task CallTool_Sse_EchoServer_Concurrently() Assert.NotNull(result); Assert.False(result.IsError); - var textContent = Assert.Single(result.Content, c => c.Type == "text"); + var textContent = Assert.Single(result.Content.OfType()); Assert.Equal($"Echo: Hello MCP! {i}", textContent.Text); } } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs index 61fc54fa..d9d98b74 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs @@ -84,7 +84,7 @@ IHttpContextAccessor is not currently supported with non-stateless Streamable HT new Dictionary() { ["message"] = "Hello world!" }, cancellationToken: TestContext.Current.CancellationToken); - var content = Assert.Single(response.Content); + var content = Assert.Single(response.Content.OfType()); Assert.Equal("TestUser: Hello world!", content.Text); } @@ -149,19 +149,14 @@ public async Task Sampling_DoesNotCloseStream_Prematurely() Assert.NotNull(parameters?.Messages); var message = Assert.Single(parameters.Messages); Assert.Equal(Role.User, message.Role); - Assert.Equal("text", message.Content.Type); - Assert.Equal("Test prompt for sampling", message.Content.Text); + Assert.Equal("Test prompt for sampling", Assert.IsType(message.Content).Text); sampleCount++; return new CreateMessageResult { Model = "test-model", Role = Role.Assistant, - Content = new Content - { - Type = "text", - Text = "Sampling response from client" - } + Content = new TextContentBlock { Text = "Sampling response from client" }, }; }, }, @@ -179,7 +174,7 @@ public async Task Sampling_DoesNotCloseStream_Prematurely() Assert.False(result.IsError); var textContent = Assert.Single(result.Content); Assert.Equal("text", textContent.Type); - Assert.Equal("Sampling completed successfully. Client responded: Sampling response from client", textContent.Text); + Assert.Equal("Sampling completed successfully. Client responded: Sampling response from client", Assert.IsType(textContent).Text); Assert.Equal(2, sampleCount); @@ -228,11 +223,7 @@ public static async Task SamplingToolAsync(IMcpServer server, string pro new SamplingMessage { Role = Role.User, - Content = new Content - { - Type = "text", - Text = prompt - }, + Content = new TextContentBlock { Text = prompt }, } ], }; @@ -240,7 +231,7 @@ public static async Task SamplingToolAsync(IMcpServer server, string pro await server.SampleAsync(samplingRequest, cancellationToken); var samplingResult = await server.SampleAsync(samplingRequest, cancellationToken); - return $"Sampling completed successfully. Client responded: {samplingResult.Content.Text}"; + return $"Sampling completed successfully. Client responded: {Assert.IsType(samplingResult.Content).Text}"; } } } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs index fc186c40..cd434d7c 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs @@ -159,7 +159,7 @@ public async Task AddMcpServer_CanBeCalled_MultipleTimes() ["message"] = "from client!" }, cancellationToken: TestContext.Current.CancellationToken); - var textContent = Assert.Single(echoResponse.Content, c => c.Type == "text"); + var textContent = Assert.Single(echoResponse.Content.OfType()); Assert.Equal("hello from client!", textContent.Text); } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs index 8786da26..1e21eb45 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs @@ -113,7 +113,7 @@ public async Task SamplingRequest_Fails_WithInvalidOperationException() var toolResponse = await client.CallToolAsync("testSamplingErrors", cancellationToken: TestContext.Current.CancellationToken); var toolContent = Assert.Single(toolResponse.Content); - Assert.Equal("Server to client requests are not supported in stateless mode.", toolContent.Text); + Assert.Equal("Server to client requests are not supported in stateless mode.", Assert.IsType(toolContent).Text); } [Fact] @@ -133,7 +133,7 @@ public async Task RootsRequest_Fails_WithInvalidOperationException() var toolResponse = await client.CallToolAsync("testRootsErrors", cancellationToken: TestContext.Current.CancellationToken); var toolContent = Assert.Single(toolResponse.Content); - Assert.Equal("Server to client requests are not supported in stateless mode.", toolContent.Text); + Assert.Equal("Server to client requests are not supported in stateless mode.", Assert.IsType(toolContent).Text); } [Fact] @@ -153,7 +153,7 @@ public async Task ElicitRequest_Fails_WithInvalidOperationException() var toolResponse = await client.CallToolAsync("testElicitationErrors", cancellationToken: TestContext.Current.CancellationToken); var toolContent = Assert.Single(toolResponse.Content); - Assert.Equal("Server to client requests are not supported in stateless mode.", toolContent.Text); + Assert.Equal("Server to client requests are not supported in stateless mode.", Assert.IsType(toolContent).Text); } [Fact] @@ -190,7 +190,7 @@ public async Task ScopedServices_Resolve_FromRequestScope() var toolResponse = await client.CallToolAsync("testScope", cancellationToken: TestContext.Current.CancellationToken); var toolContent = Assert.Single(toolResponse.Content); - Assert.Equal("From request middleware!", toolContent.Text); + Assert.Equal("From request middleware!", Assert.IsType(toolContent).Text); } [McpServerTool(Name = "testSamplingErrors")] diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs index d7f8433b..116bdc40 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs @@ -77,9 +77,9 @@ private async Task StartAsync() return Results.Json(new JsonRpcResponse { Id = request.Id, - Result = JsonSerializer.SerializeToNode(new CallToolResponse() + Result = JsonSerializer.SerializeToNode(new CallToolResult() { - Content = [new() { Text = parameters.Arguments["message"].ToString() }], + Content = [new TextContentBlock() { Text = parameters.Arguments["message"].ToString() }], }, McpJsonUtilities.DefaultOptions), }); } @@ -141,8 +141,7 @@ private static async Task CallEchoAndValidateAsync(McpClientTool echoTool) var response = await echoTool.CallAsync(new Dictionary() { ["message"] = "Hello world!" }, cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(response); var content = Assert.Single(response.Content); - Assert.Equal("text", content.Type); - Assert.Equal("Hello world!", content.Text); + Assert.Equal("Hello world!", Assert.IsType(content).Text); } public async ValueTask DisposeAsync() diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs index 3efc1041..771f6be7 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs @@ -218,9 +218,9 @@ public async Task SingleJsonRpcRequest_ThatThrowsIsHandled_WithCompleteSseRespon var response = await HttpClient.PostAsync("", JsonContent(CallTool("throw")), TestContext.Current.CancellationToken); var rpcError = await AssertSingleSseResponseAsync(response); - var error = AssertType(rpcError.Result); + var error = AssertType(rpcError.Result); var content = Assert.Single(error.Content); - Assert.Contains("'throw'", content.Text); + Assert.Contains("'throw'", Assert.IsType(content).Text); } [Fact] @@ -377,10 +377,9 @@ public async Task Progress_IsReported_InSameSseResponseAsRpcResponse() else { var rpcResponse = JsonSerializer.Deserialize(sseEvent, GetJsonTypeInfo()); - var callToolResponse = AssertType(rpcResponse?.Result); + var callToolResponse = AssertType(rpcResponse?.Result); var callToolContent = Assert.Single(callToolResponse.Content); - Assert.Equal("text", callToolContent.Type); - Assert.Equal("done", callToolContent.Text); + Assert.Equal("done", Assert.IsType(callToolContent).Text); } } @@ -413,10 +412,9 @@ async Task CallAsyncLocalToolAndValidateAsync(int expectedSessionIndex) { var response = await HttpClient.PostAsync("", JsonContent(CallTool("async-local-session")), TestContext.Current.CancellationToken); var rpcResponse = await AssertSingleSseResponseAsync(response); - var callToolResponse = AssertType(rpcResponse.Result); + var callToolResponse = AssertType(rpcResponse.Result); var callToolContent = Assert.Single(callToolResponse.Content); - Assert.Equal("text", callToolContent.Type); - Assert.Equal($"RunSessionHandler ({expectedSessionIndex})", callToolContent.Text); + Assert.Equal($"RunSessionHandler ({expectedSessionIndex})", Assert.IsType(callToolContent).Text); } await CallAsyncLocalToolAndValidateAsync(expectedSessionIndex: 0); @@ -527,7 +525,7 @@ public async Task IdleSessionsPastMaxIdleSessionCount_ArePruned_LongestIdleFirst private static T AssertType(JsonNode? jsonNode) { - var type = JsonSerializer.Deserialize(jsonNode, GetJsonTypeInfo()); + var type = JsonSerializer.Deserialize(jsonNode, GetJsonTypeInfo()); Assert.NotNull(type); return type; } @@ -603,12 +601,11 @@ private static InitializeResult AssertServerInfo(JsonRpcResponse rpcResponse) return initializeResult; } - private static CallToolResponse AssertEchoResponse(JsonRpcResponse rpcResponse) + private static CallToolResult AssertEchoResponse(JsonRpcResponse rpcResponse) { - var callToolResponse = AssertType(rpcResponse.Result); + var callToolResponse = AssertType(rpcResponse.Result); var callToolContent = Assert.Single(callToolResponse.Content); - Assert.Equal("text", callToolContent.Type); - Assert.Equal($"Hello world! ({rpcResponse.Id})", callToolContent.Text); + Assert.Equal($"Hello world! ({rpcResponse.Id})", Assert.IsType(callToolContent).Text); return callToolResponse; } diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs index 312a1823..b8d31c37 100644 --- a/tests/ModelContextProtocol.TestServer/Program.cs +++ b/tests/ModelContextProtocol.TestServer/Program.cs @@ -175,16 +175,16 @@ private static ToolsCapability ConfigureTools() { throw new McpException("Missing required argument 'message'", McpErrorCode.InvalidParams); } - return new CallToolResponse() + return new CallToolResult() { - Content = [new Content() { Text = "Echo: " + message.ToString(), Type = "text" }] + Content = [new TextContentBlock() { Text = $"Echo: {message}" }] }; } else if (request.Params?.Name == "echoSessionId") { - return new CallToolResponse() + return new CallToolResult() { - Content = [new Content() { Text = request.Server.SessionId, Type = "text" }] + Content = [new TextContentBlock() { Text = request.Server.SessionId ?? string.Empty }] }; } else if (request.Params?.Name == "sampleLLM") @@ -198,9 +198,9 @@ private static ToolsCapability ConfigureTools() var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.GetRawText())), cancellationToken); - return new CallToolResponse() + return new CallToolResult() { - Content = [new Content() { Text = $"LLM sampling result: {sampleResult.Content.Text}", Type = "text" }] + Content = [new TextContentBlock() { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] }; } else @@ -257,11 +257,7 @@ private static PromptsCapability ConfigurePrompts() messages.Add(new PromptMessage() { Role = Role.User, - Content = new Content() - { - Type = "text", - Text = "This is a simple prompt without arguments." - } + Content = new TextContentBlock() { Text = "This is a simple prompt without arguments." }, }); } else if (request.Params?.Name == "complex_prompt") @@ -271,27 +267,18 @@ private static PromptsCapability ConfigurePrompts() messages.Add(new PromptMessage() { Role = Role.User, - Content = new Content() - { - Type = "text", - Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" - } + Content = new TextContentBlock() { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, }); messages.Add(new PromptMessage() { Role = Role.Assistant, - Content = new Content() - { - Type = "text", - Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" - } + Content = new TextContentBlock() { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, }); messages.Add(new PromptMessage() { Role = Role.User, - Content = new Content() + Content = new ImageContentBlock() { - Type = "image", Data = MCP_TINY_IMAGE, MimeType = "image/png" } @@ -541,11 +528,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st Messages = [new SamplingMessage() { Role = Role.User, - Content = new Content() - { - Type = "text", - Text = $"Resource {uri} context: {context}" - } + Content = new TextContentBlock() { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs index cf078c25..d81666da 100644 --- a/tests/ModelContextProtocol.TestSseServer/Program.cs +++ b/tests/ModelContextProtocol.TestSseServer/Program.cs @@ -46,11 +46,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st Messages = [new SamplingMessage() { Role = Role.User, - Content = new Content() - { - Type = "text", - Text = $"Resource {uri} context: {context}" - } + Content = new TextContentBlock() { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, @@ -173,16 +169,16 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st { throw new McpException("Missing required argument 'message'", McpErrorCode.InvalidParams); } - return new CallToolResponse() + return new CallToolResult() { - Content = [new Content() { Text = "Echo: " + message.ToString(), Type = "text" }] + Content = [new TextContentBlock() { Text = $"Echo: {message}" }] }; } else if (request.Params.Name == "echoSessionId") { - return new CallToolResponse() + return new CallToolResult() { - Content = [new Content() { Text = request.Server.SessionId, Type = "text" }] + Content = [new TextContentBlock() { Text = request.Server.SessionId ?? string.Empty }] }; } else if (request.Params.Name == "sampleLLM") @@ -196,9 +192,9 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())), cancellationToken); - return new CallToolResponse() + return new CallToolResult() { - Content = [new Content() { Text = $"LLM sampling result: {sampleResult.Content.Text}", Type = "text" }] + Content = [new TextContentBlock() { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] }; } else @@ -339,11 +335,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st messages.Add(new PromptMessage() { Role = Role.User, - Content = new Content() - { - Type = "text", - Text = "This is a simple prompt without arguments." - } + Content = new TextContentBlock() { Text = "This is a simple prompt without arguments." }, }); } else if (request.Params.Name == "complex_prompt") @@ -353,27 +345,18 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st messages.Add(new PromptMessage() { Role = Role.User, - Content = new Content() - { - Type = "text", - Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" - } + Content = new TextContentBlock() { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, }); messages.Add(new PromptMessage() { Role = Role.Assistant, - Content = new Content() - { - Type = "text", - Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" - } + Content = new TextContentBlock() { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, }); messages.Add(new PromptMessage() { Role = Role.User, - Content = new Content() + Content = new ImageContentBlock() { - Type = "image", Data = MCP_TINY_IMAGE, MimeType = "image/png" } diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs index bc1787ea..96da34cf 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs @@ -44,7 +44,7 @@ public async Task CreateSamplingHandler_ShouldHandleTextMessages(float? temperat new SamplingMessage { Role = Role.User, - Content = new Content { Type = "text", Text = "Hello" } + Content = new TextContentBlock { Text = "Hello" } } ], Temperature = temperature, @@ -80,7 +80,7 @@ public async Task CreateSamplingHandler_ShouldHandleTextMessages(float? temperat // Assert Assert.NotNull(result); - Assert.Equal("Hello, World!", result.Content.Text); + Assert.Equal("Hello, World!", (result.Content as TextContentBlock)?.Text); Assert.Equal("test-model", result.Model); Assert.Equal(Role.Assistant, result.Role); Assert.Equal("endTurn", result.StopReason); @@ -98,9 +98,8 @@ public async Task CreateSamplingHandler_ShouldHandleImageMessages() new SamplingMessage { Role = Role.User, - Content = new Content + Content = new ImageContentBlock { - Type = "image", MimeType = "image/png", Data = Convert.ToBase64String(new byte[] { 1, 2, 3 }) } @@ -135,7 +134,7 @@ public async Task CreateSamplingHandler_ShouldHandleImageMessages() // Assert Assert.NotNull(result); - Assert.Equal(expectedData, result.Content.Data); + Assert.Equal(expectedData, (result.Content as ImageContentBlock)?.Data); Assert.Equal("test-model", result.Model); Assert.Equal(Role.Assistant, result.Role); Assert.Equal("endTurn", result.StopReason); @@ -162,11 +161,7 @@ public async Task CreateSamplingHandler_ShouldHandleResourceMessages() new SamplingMessage { Role = Role.User, - Content = new Content - { - Type = "resource", - Resource = resource - }, + Content = new EmbeddedResourceBlock { Resource = resource }, } ], MaxTokens = 100 @@ -284,7 +279,7 @@ public async Task SendRequestAsync_HonorsJsonSerializerOptions() JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; await using IMcpClient client = await CreateMcpClientForServer(); - await Assert.ThrowsAsync(async () => await client.SendRequestAsync("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await client.SendRequestAsync("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs index 9b7f4456..7516a218 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs @@ -70,7 +70,7 @@ public async Task CreateAsync_WithCapabilitiesOptions(Type transportType) SamplingHandler = async (c, p, t) => new CreateMessageResult { - Content = new Content { Text = "result" }, + Content = new TextContentBlock { Text = "result" }, Model = "test-model", Role = Role.User, StopReason = "endTurn" diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index dfd8d767..f909db18 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -92,12 +92,12 @@ public async Task CallTool_Stdio_EchoServer(string clientId) // assert Assert.NotNull(result); Assert.False(result.IsError); - var textContent = Assert.Single(result.Content, c => c.Type == "text"); + var textContent = Assert.Single(result.Content.OfType()); Assert.Equal("Echo: Hello MCP!", textContent.Text); } [Fact] - public async Task CallTool_Stdio_EchoSessionId_ReturnsNull() + public async Task CallTool_Stdio_EchoSessionId_ReturnsEmpty() { // arrange @@ -108,8 +108,8 @@ public async Task CallTool_Stdio_EchoSessionId_ReturnsNull() // assert Assert.NotNull(result); Assert.False(result.IsError); - var textContent = Assert.Single(result.Content, c => c.Type == "text"); - Assert.Null(textContent.Text); + var textContent = Assert.Single(result.Content.OfType()); + Assert.Empty(textContent.Text); } [Theory] @@ -326,7 +326,7 @@ public async Task UnsubscribeResource_Stdio() [Theory] [MemberData(nameof(GetClients))] - public async Task Complete_Stdio_ResourceReference(string clientId) + public async Task Complete_Stdio_ResourceTemplateReference(string clientId) { // arrange @@ -387,11 +387,7 @@ public async Task Sampling_Stdio(string clientId) { Model = "test-model", Role = Role.Assistant, - Content = new Content - { - Type = "text", - Text = "Test response" - } + Content = new TextContentBlock { Text = "Test response" }, }; }, }, @@ -410,8 +406,7 @@ public async Task Sampling_Stdio(string clientId) // assert Assert.NotNull(result); - var textContent = Assert.Single(result.Content); - Assert.Equal("text", textContent.Type); + var textContent = Assert.Single(result.Content.OfType()); Assert.False(string.IsNullOrEmpty(textContent.Text)); } @@ -556,9 +551,9 @@ public async Task SamplingViaChatClient_RequestResponseProperlyPropagated() Assert.NotNull(result); Assert.NotEmpty(result.Content); - Assert.Equal("text", result.Content[0].Type); - Assert.Contains("LLM sampling result:", result.Content[0].Text); - Assert.Contains("Eiffel", result.Content[0].Text); + var content = Assert.IsType(result.Content[0]); + Assert.Contains("LLM sampling result:", content.Text); + Assert.Contains("Eiffel", content.Text); } [Theory] diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsHandlerTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsHandlerTests.cs index 45e58fe5..c446eb5d 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsHandlerTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsHandlerTests.cs @@ -34,7 +34,7 @@ public void WithListToolsHandler_Sets_Handler() [Fact] public void WithCallToolHandler_Sets_Handler() { - Func, CancellationToken, ValueTask> handler = async (context, token) => new CallToolResponse(); + Func, CancellationToken, ValueTask> handler = async (context, token) => new CallToolResult(); _builder.Object.WithCallToolHandler(handler); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index 501730b8..a4747185 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -71,7 +71,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer case "FinalCustomPrompt": return new GetPromptResult() { - Messages = [new() { Role = Role.User, Content = new() { Text = $"hello from {request.Params.Name}", Type = "text" } }], + Messages = [new() { Role = Role.User, Content = new TextContentBlock() { Text = $"hello from {request.Params.Name}" } }], }; default: @@ -276,7 +276,7 @@ public static PromptMessage AnotherPrompt(ObjectWithId id) => new PromptMessage { Role = Role.User, - Content = new() { Text = "hello", Type = "text" }, + Content = new TextContentBlock() { Text = "hello" }, }; } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index 010d1b0e..2a517919 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -95,9 +95,9 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer case "FirstCustomTool": case "SecondCustomTool": case "FinalCustomTool": - return new CallToolResponse() + return new CallToolResult() { - Content = [new Content() { Text = $"{request.Params.Name}Result", Type = "text" }], + Content = [new TextContentBlock() { Text = $"{request.Params.Name}Result" }], }; default: @@ -239,8 +239,9 @@ public async Task Can_Call_Registered_Tool() Assert.NotNull(result.Content); Assert.NotEmpty(result.Content); - Assert.Equal("hello Peter", result.Content[0].Text); - Assert.Equal("text", result.Content[0].Type); + var tc = Assert.IsType(result.Content[0]); + Assert.Equal("hello Peter", tc.Text); + Assert.Equal("text", tc.Type); } [Fact] @@ -255,8 +256,8 @@ public async Task Can_Call_Registered_Tool_With_Array_Result() Assert.NotNull(result.Content); Assert.NotEmpty(result.Content); - Assert.Equal("hello Peter", result.Content[0].Text); - Assert.Equal("hello2 Peter", result.Content[1].Text); + Assert.Equal("hello Peter", (result.Content[0] as TextContentBlock)?.Text); + Assert.Equal("hello2 Peter", (result.Content[1] as TextContentBlock)?.Text); result = await client.CallToolAsync( "SecondCustomTool", @@ -264,7 +265,7 @@ public async Task Can_Call_Registered_Tool_With_Array_Result() Assert.NotNull(result); Assert.NotNull(result.Content); Assert.NotEmpty(result.Content); - Assert.Equal("SecondCustomToolResult", result.Content[0].Text); + Assert.Equal("SecondCustomToolResult", (result.Content[0] as TextContentBlock)?.Text); } [Fact] @@ -294,8 +295,8 @@ public async Task Can_Call_Registered_Tool_With_Json_Result() Assert.NotNull(result.Content); Assert.NotEmpty(result.Content); - Assert.Equal("""{"SomeProp":false}""", Regex.Replace(result.Content[0].Text ?? string.Empty, "\\s+", "")); - Assert.Equal("text", result.Content[0].Type); + Assert.Equal("""{"SomeProp":false}""", Regex.Replace((result.Content[0] as TextContentBlock)?.Text ?? string.Empty, "\\s+", "")); + Assert.Equal("text", (result.Content[0] as TextContentBlock)?.Type); } [Fact] @@ -310,8 +311,7 @@ public async Task Can_Call_Registered_Tool_With_Int_Result() Assert.NotNull(result.Content); Assert.NotEmpty(result.Content); - Assert.Equal("5", result.Content[0].Text); - Assert.Equal("text", result.Content[0].Type); + Assert.Equal("5", (result.Content[0] as TextContentBlock)?.Text); } [Fact] @@ -328,8 +328,7 @@ public async Task Can_Call_Registered_Tool_And_Pass_ComplexType() Assert.NotNull(result.Content); Assert.NotEmpty(result.Content); - Assert.Equal("Peter", result.Content[0].Text); - Assert.Equal("text", result.Content[0].Type); + Assert.Equal("Peter", (result.Content[0] as TextContentBlock)?.Text); } [Fact] @@ -348,7 +347,7 @@ public async Task Can_Call_Registered_Tool_With_Instance_Method() Assert.NotNull(result.Content); Assert.NotEmpty(result.Content); - parts[i] = result.Content[0].Text?.Split(':') ?? []; + parts[i] = (result.Content[0] as TextContentBlock)?.Text?.Split(':') ?? []; Assert.Equal(2, parts[i].Length); } @@ -373,7 +372,7 @@ public async Task Returns_IsError_Content_When_Tool_Fails() Assert.True(result.IsError); Assert.NotNull(result.Content); Assert.NotEmpty(result.Content); - Assert.Contains("An error occurred", result.Content[0].Text); + Assert.Contains("An error occurred", (result.Content[0] as TextContentBlock)?.Text); } [Fact] @@ -612,7 +611,7 @@ public async Task HandlesIProgressParameter() return default; })) { - var result = await client.SendRequestAsync( + var result = await client.SendRequestAsync( RequestMethods.ToolsCall, new CallToolRequestParams { @@ -647,7 +646,7 @@ public async Task CancellationNotificationsPropagateToToolTokens() McpClientTool cancelableTool = tools.First(t => t.Name == nameof(EchoTool.InfiniteCancelableOperation)); var requestId = new RequestId(Guid.NewGuid().ToString()); - var invokeTask = client.SendRequestAsync( + var invokeTask = client.SendRequestAsync( RequestMethods.ToolsCall, new CallToolRequestParams { Name = cancelableTool.ProtocolTool.Name }, requestId: requestId, diff --git a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs index acc52904..ffd95076 100644 --- a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs +++ b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs @@ -83,11 +83,7 @@ public async Task Sampling_Sse_EverythingServer() { Model = "test-model", Role = Role.Assistant, - Content = new Content - { - Type = "text", - Text = "Test response" - } + Content = new TextContentBlock { Text = "Test response" }, }; }, }, @@ -109,7 +105,7 @@ public async Task Sampling_Sse_EverythingServer() // assert Assert.NotNull(result); - var textContent = Assert.Single(result.Content); + var textContent = Assert.Single(result.Content.OfType()); Assert.Equal("text", textContent.Type); Assert.False(string.IsNullOrEmpty(textContent.Text)); } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerDelegatesTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerDelegatesTests.cs index cb7d48a9..97b63157 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerDelegatesTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerDelegatesTests.cs @@ -22,7 +22,7 @@ public void AllPropertiesAreSettable() Assert.Null(handlers.UnsubscribeFromResourcesHandler); handlers.ListToolsHandler = async (p, c) => new ListToolsResult(); - handlers.CallToolHandler = async (p, c) => new CallToolResponse(); + handlers.CallToolHandler = async (p, c) => new CallToolResult(); handlers.ListPromptsHandler = async (p, c) => new ListPromptsResult(); handlers.GetPromptHandler = async (p, c) => new GetPromptResult(); handlers.ListResourceTemplatesHandler = async (p, c) => new ListResourceTemplatesResult(); diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs index 1cd9b4ed..c3287829 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs @@ -41,7 +41,7 @@ public async Task SupportsIMcpServer() Assert.NotNull(result); Assert.NotNull(result.Messages); Assert.Single(result.Messages); - Assert.Equal("Hello", result.Messages[0].Content.Text); + Assert.Equal("Hello", Assert.IsType(result.Messages[0].Content).Text); } [Fact] @@ -56,7 +56,7 @@ public async Task SupportsServiceFromDI() McpServerPrompt prompt = McpServerPrompt.Create((MyService actualMyService, int? something = null) => { Assert.Same(expectedMyService, actualMyService); - return new PromptMessage() { Role = Role.Assistant, Content = new() { Text = "Hello", Type = "text" } }; + return new PromptMessage() { Role = Role.Assistant, Content = new TextContentBlock() { Text = "Hello" } }; }, new() { Services = services }); Assert.Contains("something", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []); @@ -69,7 +69,7 @@ await Assert.ThrowsAnyAsync(async () => await prompt.GetAsync var result = await prompt.GetAsync( new RequestContext(new Mock().Object) { Services = services }, TestContext.Current.CancellationToken); - Assert.Equal("Hello", result.Messages[0].Content.Text); + Assert.Equal("Hello", Assert.IsType(result.Messages[0].Content).Text); } [Fact] @@ -84,13 +84,13 @@ public async Task SupportsOptionalServiceFromDI() McpServerPrompt prompt = McpServerPrompt.Create((MyService? actualMyService = null) => { Assert.Null(actualMyService); - return new PromptMessage() { Role = Role.Assistant, Content = new() { Text = "Hello", Type = "text" } }; + return new PromptMessage() { Role = Role.Assistant, Content = new TextContentBlock() { Text = "Hello" } }; }, new() { Services = services }); var result = await prompt.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); - Assert.Equal("Hello", result.Messages[0].Content.Text); + Assert.Equal("Hello", Assert.IsType(result.Messages[0].Content).Text); } [Fact] @@ -103,7 +103,7 @@ public async Task SupportsDisposingInstantiatedDisposableTargets() var result = await prompt1.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); - Assert.Equal("disposals:1", result.Messages[0].Content.Text); + Assert.Equal("disposals:1", Assert.IsType(result.Messages[0].Content).Text); } [Fact] @@ -116,7 +116,7 @@ public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableTargets() var result = await prompt1.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); - Assert.Equal("asyncDisposals:1", result.Messages[0].Content.Text); + Assert.Equal("asyncDisposals:1", Assert.IsType(result.Messages[0].Content).Text); } [Fact] @@ -129,7 +129,7 @@ public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposable var result = await prompt1.GetAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); - Assert.Equal("disposals:0, asyncDisposals:1", result.Messages[0].Content.Text); + Assert.Equal("disposals:0, asyncDisposals:1", Assert.IsType(result.Messages[0].Content).Text); } [Fact] @@ -168,7 +168,7 @@ public async Task CanReturnText() Assert.Single(actual.Messages); Assert.Equal(Role.User, actual.Messages[0].Role); Assert.Equal("text", actual.Messages[0].Content.Type); - Assert.Equal(expected, actual.Messages[0].Content.Text); + Assert.Equal(expected, Assert.IsType(actual.Messages[0].Content).Text); } [Fact] @@ -177,7 +177,7 @@ public async Task CanReturnPromptMessage() PromptMessage expected = new() { Role = Role.User, - Content = new() { Text = "hello", Type = "text" } + Content = new TextContentBlock() { Text = "hello" } }; McpServerPrompt prompt = McpServerPrompt.Create(() => @@ -202,12 +202,12 @@ public async Task CanReturnPromptMessages() new() { Role = Role.User, - Content = new() { Text = "hello", Type = "text" } + Content = new TextContentBlock() { Text = "hello" } }, new() { Role = Role.Assistant, - Content = new() { Text = "hello again", Type = "text" } + Content = new TextContentBlock() { Text = "hello again" } } ]; @@ -224,11 +224,9 @@ public async Task CanReturnPromptMessages() Assert.NotNull(actual.Messages); Assert.Equal(2, actual.Messages.Count); Assert.Equal(Role.User, actual.Messages[0].Role); - Assert.Equal("text", actual.Messages[0].Content.Type); - Assert.Equal("hello", actual.Messages[0].Content.Text); + Assert.Equal("hello", Assert.IsType(actual.Messages[0].Content).Text); Assert.Equal(Role.Assistant, actual.Messages[1].Role); - Assert.Equal("text", actual.Messages[1].Content.Type); - Assert.Equal("hello again", actual.Messages[1].Content.Text); + Assert.Equal("hello again", Assert.IsType(actual.Messages[1].Content).Text); } [Fact] @@ -237,7 +235,7 @@ public async Task CanReturnChatMessage() PromptMessage expected = new() { Role = Role.User, - Content = new() { Text = "hello", Type = "text" } + Content = new TextContentBlock() { Text = "hello" } }; McpServerPrompt prompt = McpServerPrompt.Create(() => @@ -253,8 +251,7 @@ public async Task CanReturnChatMessage() Assert.NotNull(actual.Messages); Assert.Single(actual.Messages); Assert.Equal(Role.User, actual.Messages[0].Role); - Assert.Equal("text", actual.Messages[0].Content.Type); - Assert.Equal("hello", actual.Messages[0].Content.Text); + Assert.Equal("hello", Assert.IsType(actual.Messages[0].Content).Text); } [Fact] @@ -264,12 +261,12 @@ public async Task CanReturnChatMessages() new() { Role = Role.User, - Content = new() { Text = "hello", Type = "text" } + Content = new TextContentBlock() { Text = "hello" } }, new() { Role = Role.Assistant, - Content = new() { Text = "hello again", Type = "text" } + Content = new TextContentBlock() { Text = "hello again" } } ]; @@ -286,11 +283,9 @@ public async Task CanReturnChatMessages() Assert.NotNull(actual.Messages); Assert.Equal(2, actual.Messages.Count); Assert.Equal(Role.User, actual.Messages[0].Role); - Assert.Equal("text", actual.Messages[0].Content.Type); - Assert.Equal("hello", actual.Messages[0].Content.Text); + Assert.Equal("hello", Assert.IsType(actual.Messages[0].Content).Text); Assert.Equal(Role.Assistant, actual.Messages[1].Role); - Assert.Equal("text", actual.Messages[1].Content.Type); - Assert.Equal("hello again", actual.Messages[1].Content.Text); + Assert.Equal("hello again", Assert.IsType(actual.Messages[1].Content).Text); } [Fact] diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 0e1bb429..03fae211 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -474,9 +474,9 @@ await Can_Handle_Requests( { CallToolHandler = async (request, ct) => { - return new CallToolResponse + return new CallToolResult { - Content = [new Content { Text = "test" }] + Content = [new TextContentBlock { Text = "test" }] }; }, ListToolsHandler = (request, ct) => throw new NotImplementedException(), @@ -486,10 +486,10 @@ await Can_Handle_Requests( configureOptions: null, assertResult: response => { - var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); + var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); Assert.NotNull(result); Assert.NotEmpty(result.Content); - Assert.Equal("test", result.Content[0].Text); + Assert.Equal("test", Assert.IsType(result.Content[0]).Text); }); } @@ -630,12 +630,12 @@ public Task SendRequestAsync(JsonRpcRequest request, Cancellati Assert.Equal($"You are a helpful assistant.{Environment.NewLine}More system stuff.", rp.SystemPrompt); Assert.Equal(2, rp.Messages.Count); - Assert.Equal("I am going to France.", rp.Messages[0].Content.Text); - Assert.Equal("What is the most famous tower in Paris?", rp.Messages[1].Content.Text); + Assert.Equal("I am going to France.", Assert.IsType(rp.Messages[0].Content).Text); + Assert.Equal("What is the most famous tower in Paris?", Assert.IsType(rp.Messages[1].Content).Text); CreateMessageResult result = new() { - Content = new() { Text = "The Eiffel Tower.", Type = "text" }, + Content = new TextContentBlock() { Text = "The Eiffel Tower." }, Model = "amazingmodel", Role = Role.Assistant, StopReason = "endTurn", diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs index 5a390cfe..24260b97 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs @@ -47,7 +47,7 @@ public async Task SupportsIMcpServer() var result = await tool.InvokeAsync( new RequestContext(mockServer.Object), TestContext.Current.CancellationToken); - Assert.Equal("42", result.Content[0].Text); + Assert.Equal("42", (result.Content[0] as TextContentBlock)?.Text); } [Theory] @@ -104,7 +104,7 @@ public async Task SupportsServiceFromDI(ServiceLifetime injectedArgumentLifetime result = await tool.InvokeAsync( new RequestContext(mockServer.Object) { Services = services }, TestContext.Current.CancellationToken); - Assert.Equal("42", result.Content[0].Text); + Assert.Equal("42", (result.Content[0] as TextContentBlock)?.Text); } [Fact] @@ -125,7 +125,7 @@ public async Task SupportsOptionalServiceFromDI() var result = await tool.InvokeAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); - Assert.Equal("42", result.Content[0].Text); + Assert.Equal("42", (result.Content[0] as TextContentBlock)?.Text); } [Fact] @@ -140,7 +140,7 @@ public async Task SupportsDisposingInstantiatedDisposableTargets() var result = await tool1.InvokeAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); - Assert.Equal("""{"disposals":1}""", result.Content[0].Text); + Assert.Equal("""{"disposals":1}""", (result.Content[0] as TextContentBlock)?.Text); } [Fact] @@ -155,7 +155,7 @@ public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableTargets() var result = await tool1.InvokeAsync( new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); - Assert.Equal("""{"asyncDisposals":1}""", result.Content[0].Text); + Assert.Equal("""{"asyncDisposals":1}""", (result.Content[0] as TextContentBlock)?.Text); } [Fact] @@ -174,7 +174,7 @@ public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposable var result = await tool1.InvokeAsync( new RequestContext(new Mock().Object) { Services = services }, TestContext.Current.CancellationToken); - Assert.Equal("""{"asyncDisposals":1,"disposals":0}""", result.Content[0].Text); + Assert.Equal("""{"asyncDisposals":1,"disposals":0}""", (result.Content[0] as TextContentBlock)?.Text); } @@ -198,16 +198,13 @@ public async Task CanReturnCollectionOfAIContent() Assert.Equal(3, result.Content.Count); - Assert.Equal("text", result.Content[0].Text); - Assert.Equal("text", result.Content[0].Type); + Assert.Equal("text", (result.Content[0] as TextContentBlock)?.Text); - Assert.Equal("1234", result.Content[1].Data); - Assert.Equal("image/png", result.Content[1].MimeType); - Assert.Equal("image", result.Content[1].Type); + Assert.Equal("1234", (result.Content[1] as ImageContentBlock)?.Data); + Assert.Equal("image/png", (result.Content[1] as ImageContentBlock)?.MimeType); - Assert.Equal("1234", result.Content[2].Data); - Assert.Equal("audio/wav", result.Content[2].MimeType); - Assert.Equal("audio", result.Content[2].Type); + Assert.Equal("1234", (result.Content[2] as AudioContentBlock)?.Data); + Assert.Equal("audio/wav", (result.Content[2] as AudioContentBlock)?.MimeType); } [Theory] @@ -236,15 +233,23 @@ public async Task CanReturnSingleAIContent(string data, string type) Assert.Single(result.Content); Assert.Equal(type, result.Content[0].Type); - if (type != "text") + if (result.Content[0] is TextContentBlock tc) + { + Assert.Equal(data, tc.Text); + } + else if (result.Content[0] is ImageContentBlock ic) + { + Assert.Equal(data.Split(',').Last(), ic.Data); + Assert.Equal("image/png", ic.MimeType); + } + else if (result.Content[0] is AudioContentBlock ac) { - Assert.NotNull(result.Content[0].MimeType); - Assert.Equal(data.Split(',').Last(), result.Content[0].Data); + Assert.Equal(data.Split(',').Last(), ac.Data); + Assert.Equal("audio/wav", ac.MimeType); } else { - Assert.Null(result.Content[0].MimeType); - Assert.Equal(data, result.Content[0].Text); + throw new XunitException($"Unexpected content type: {result.Content[0].GetType()}"); } } @@ -276,8 +281,7 @@ public async Task CanReturnString() new RequestContext(mockServer.Object), TestContext.Current.CancellationToken); Assert.Single(result.Content); - Assert.Equal("42", result.Content[0].Text); - Assert.Equal("text", result.Content[0].Type); + Assert.Equal("42", Assert.IsType(result.Content[0]).Text); } [Fact] @@ -293,10 +297,8 @@ public async Task CanReturnCollectionOfStrings() new RequestContext(mockServer.Object), TestContext.Current.CancellationToken); Assert.Equal(2, result.Content.Count); - Assert.Equal("42", result.Content[0].Text); - Assert.Equal("text", result.Content[0].Type); - Assert.Equal("43", result.Content[1].Text); - Assert.Equal("text", result.Content[1].Type); + Assert.Equal("42", Assert.IsType(result.Content[0]).Text); + Assert.Equal("43", Assert.IsType(result.Content[1]).Text); } [Fact] @@ -306,13 +308,13 @@ public async Task CanReturnMcpContent() McpServerTool tool = McpServerTool.Create((IMcpServer server) => { Assert.Same(mockServer.Object, server); - return new Content { Text = "42", Type = "text" }; + return new TextContentBlock { Text = "42" }; }); var result = await tool.InvokeAsync( new RequestContext(mockServer.Object), TestContext.Current.CancellationToken); Assert.Single(result.Content); - Assert.Equal("42", result.Content[0].Text); + Assert.Equal("42", Assert.IsType(result.Content[0]).Text); Assert.Equal("text", result.Content[0].Type); } @@ -323,26 +325,23 @@ public async Task CanReturnCollectionOfMcpContent() McpServerTool tool = McpServerTool.Create((IMcpServer server) => { Assert.Same(mockServer.Object, server); - return new List() { new() { Text = "42", Type = "text" }, new() { Data = "1234", Type = "image", MimeType = "image/png" } }; + return new List() { new TextContentBlock() { Text = "42" }, new ImageContentBlock() { Data = "1234", MimeType = "image/png" } }; }); var result = await tool.InvokeAsync( new RequestContext(mockServer.Object), TestContext.Current.CancellationToken); Assert.Equal(2, result.Content.Count); - Assert.Equal("42", result.Content[0].Text); - Assert.Equal("text", result.Content[0].Type); - Assert.Equal("1234", result.Content[1].Data); - Assert.Equal("image", result.Content[1].Type); - Assert.Equal("image/png", result.Content[1].MimeType); - Assert.Null(result.Content[1].Text); + Assert.Equal("42", Assert.IsType(result.Content[0]).Text); + Assert.Equal("1234", Assert.IsType(result.Content[1]).Data); + Assert.Equal("image/png", Assert.IsType(result.Content[1]).MimeType); } [Fact] - public async Task CanReturnCallToolResponse() + public async Task CanReturnCallToolResult() { - CallToolResponse response = new() + CallToolResult response = new() { - Content = [new() { Text = "text", Type = "text" }, new() { Data = "1234", Type = "image" }] + Content = new List() { new TextContentBlock { Text = "text" }, new ImageContentBlock { Data = "1234", MimeType = "image/png" } } }; Mock mockServer = new(); @@ -358,10 +357,8 @@ public async Task CanReturnCallToolResponse() Assert.Same(response, result); Assert.Equal(2, result.Content.Count); - Assert.Equal("text", result.Content[0].Text); - Assert.Equal("text", result.Content[0].Type); - Assert.Equal("1234", result.Content[1].Data); - Assert.Equal("image", result.Content[1].Type); + Assert.Equal("text", Assert.IsType(result.Content[0]).Text); + Assert.Equal("1234", Assert.IsType(result.Content[1]).Data); } [Fact] @@ -418,7 +415,7 @@ public async Task ToolCallError_LogsErrorMessage() // Assert Assert.True(result.IsError); Assert.Single(result.Content); - Assert.Equal($"An error occurred invoking '{toolName}'.", result.Content[0].Text); + Assert.Equal($"An error occurred invoking '{toolName}'.", Assert.IsType(result.Content[0]).Text); var errorLog = Assert.Single(mockLoggerProvider.LogMessages, m => m.LogLevel == LogLevel.Error); Assert.Equal($"\"{toolName}\" threw an unhandled exception.", errorLog.Message); From e9caa71aee52a78b5927920a279f5d8476afa743 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 13 Jun 2025 16:15:08 -0400 Subject: [PATCH 2/4] Add Title to a bunch of types --- samples/EverythingServer/Program.cs | 6 +- .../CodeAnalysis/StringSyntaxAttribute.cs | 68 +++++++ .../Client/McpClient.cs | 1 - .../Client/McpClientExtensions.cs | 5 - .../Client/McpClientPrompt.cs | 3 + .../Client/McpClientResource.cs | 3 + .../Client/McpClientResourceTemplate.cs | 3 + .../Client/McpClientTool.cs | 3 + .../McpJsonUtilities.cs | 2 - .../Protocol/IBaseMetadata.cs | 25 +++ .../Protocol/Implementation.cs | 13 +- .../Protocol/Prompt.cs | 16 +- .../Protocol/PromptArgument.cs | 12 +- .../Protocol/Reference.cs | 190 +++++++++++++----- .../Protocol/Resource.cs | 12 +- .../Protocol/ResourceTemplate.cs | 17 +- .../ResourceUpdatedNotificationParams.cs | 2 + .../Protocol/SubscribeRequestParams.cs | 2 + .../Protocol/Tool.cs | 10 +- .../Protocol/UnsubscribeRequestParams.cs | 2 + .../Server/AIFunctionMcpServerPrompt.cs | 2 + .../Server/AIFunctionMcpServerResource.cs | 2 + .../Server/AIFunctionMcpServerTool.cs | 12 +- .../Server/McpServerPromptAttribute.cs | 3 + .../Server/McpServerPromptCreateOptions.cs | 6 + .../Server/McpServerPromptTypeAttribute.cs | 2 - .../Server/McpServerResourceAttribute.cs | 3 + .../Server/McpServerResourceCreateOptions.cs | 6 + .../Server/McpServerResourceTypeAttribute.cs | 2 - .../Server/McpServerToolTypeAttribute.cs | 2 - .../Server/RequestContext.cs | 2 - .../Program.cs | 34 ++-- .../ClientIntegrationTests.cs | 14 +- .../McpServerBuilderExtensionsPromptsTests.cs | 16 +- ...cpServerBuilderExtensionsResourcesTests.cs | 22 +- .../McpServerBuilderExtensionsToolsTests.cs | 19 +- ...pServerBuilderExtensionsTransportsTests.cs | 1 - .../Transport/StdioServerTransportTests.cs | 3 +- 38 files changed, 407 insertions(+), 139 deletions(-) create mode 100644 src/Common/Polyfills/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs create mode 100644 src/ModelContextProtocol.Core/Protocol/IBaseMetadata.cs diff --git a/samples/EverythingServer/Program.cs b/samples/EverythingServer/Program.cs index d46e40b5..b976bcc0 100644 --- a/samples/EverythingServer/Program.cs +++ b/samples/EverythingServer/Program.cs @@ -86,9 +86,9 @@ await ctx.Server.SampleAsync([ var @ref = @params.Ref; var argument = @params.Argument; - if (@ref.Type == "ref/resource") + if (@ref is ResourceTemplateReference rtr) { - var resourceId = @ref.Uri?.Split("/").Last(); + var resourceId = rtr.Uri?.Split("/").Last(); if (resourceId is null) { @@ -103,7 +103,7 @@ await ctx.Server.SampleAsync([ }; } - if (@ref.Type == "ref/prompt") + if (@ref is PromptReference pr) { if (!exampleCompletions.TryGetValue(argument.Name, out IEnumerable? value)) { diff --git a/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs b/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs new file mode 100644 index 00000000..a8ab9bd2 --- /dev/null +++ b/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis; + +/// Specifies the syntax used in a string. +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +internal sealed class StringSyntaxAttribute : Attribute +{ + /// Initializes the with the identifier of the syntax used. + /// The syntax identifier. + public StringSyntaxAttribute(string syntax) + { + Syntax = syntax; + Arguments = Array.Empty(); + } + + /// Initializes the with the identifier of the syntax used. + /// The syntax identifier. + /// Optional arguments associated with the specific syntax employed. + public StringSyntaxAttribute(string syntax, params object?[] arguments) + { + Syntax = syntax; + Arguments = arguments; + } + + /// Gets the identifier of the syntax used. + public string Syntax { get; } + + /// Optional arguments associated with the specific syntax employed. + public object?[] Arguments { get; } + + /// The syntax identifier for strings containing composite formats for string formatting. + public const string CompositeFormat = nameof(CompositeFormat); + + /// The syntax identifier for strings containing date format specifiers. + public const string DateOnlyFormat = nameof(DateOnlyFormat); + + /// The syntax identifier for strings containing date and time format specifiers. + public const string DateTimeFormat = nameof(DateTimeFormat); + + /// The syntax identifier for strings containing format specifiers. + public const string EnumFormat = nameof(EnumFormat); + + /// The syntax identifier for strings containing format specifiers. + public const string GuidFormat = nameof(GuidFormat); + + /// The syntax identifier for strings containing JavaScript Object Notation (JSON). + public const string Json = nameof(Json); + + /// The syntax identifier for strings containing numeric format specifiers. + public const string NumericFormat = nameof(NumericFormat); + + /// The syntax identifier for strings containing regular expressions. + public const string Regex = nameof(Regex); + + /// The syntax identifier for strings containing time format specifiers. + public const string TimeOnlyFormat = nameof(TimeOnlyFormat); + + /// The syntax identifier for strings containing format specifiers. + public const string TimeSpanFormat = nameof(TimeSpanFormat); + + /// The syntax identifier for strings containing URIs. + public const string Uri = nameof(Uri); + + /// The syntax identifier for strings containing XML. + public const string Xml = nameof(Xml); +} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/McpClient.cs b/src/ModelContextProtocol.Core/Client/McpClient.cs index 43639db2..c4f23238 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; -using System.Diagnostics; using System.Text.Json; namespace ModelContextProtocol.Client; diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index 803388c4..df7ca2d6 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -639,11 +639,6 @@ public static ValueTask CompleteAsync(this IMcpClient client, Re Throw.IfNull(reference); Throw.IfNullOrWhiteSpace(argumentName); - if (!reference.Validate(out string? validationMessage)) - { - throw new ArgumentException($"Invalid reference: {validationMessage}", nameof(reference)); - } - return client.SendRequestAsync( RequestMethods.CompletionComplete, new() diff --git a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs index d8c4e41e..43fc759a 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs @@ -44,6 +44,9 @@ internal McpClientPrompt(IMcpClient client, Prompt prompt) /// Gets the name of the prompt. public string Name => ProtocolPrompt.Name; + /// Gets the title of the prompt. + public string? Title => ProtocolPrompt.Title; + /// Gets a description of the prompt. public string? Description => ProtocolPrompt.Description; diff --git a/src/ModelContextProtocol.Core/Client/McpClientResource.cs b/src/ModelContextProtocol.Core/Client/McpClientResource.cs index 1fbb9927..06f8aff6 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResource.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResource.cs @@ -42,6 +42,9 @@ internal McpClientResource(IMcpClient client, Resource resource) /// Gets the name of the resource. public string Name => ProtocolResource.Name; + /// Gets the title of the resource. + public string? Title => ProtocolResource.Title; + /// Gets a description of the resource. public string? Description => ProtocolResource.Description; diff --git a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs index fe02245f..4da1bd0c 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs @@ -42,6 +42,9 @@ internal McpClientResourceTemplate(IMcpClient client, ResourceTemplate resourceT /// Gets the name of the resource template. public string Name => ProtocolResourceTemplate.Name; + /// Gets the title of the resource template. + public string? Title => ProtocolResourceTemplate.Title; + /// Gets a description of the resource template. public string? Description => ProtocolResourceTemplate.Description; diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs index e43d46da..a3cf7a46 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs @@ -68,6 +68,9 @@ internal McpClientTool( /// public override string Name => _name; + /// Gets the tool's title. + public string? Title => ProtocolTool.Title ?? ProtocolTool.Annotations?.Title; + /// public override string Description => _description; diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs index 0ce13888..1fd3ce4c 100644 --- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs +++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs @@ -1,8 +1,6 @@ using Microsoft.Extensions.AI; using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; diff --git a/src/ModelContextProtocol.Core/Protocol/IBaseMetadata.cs b/src/ModelContextProtocol.Core/Protocol/IBaseMetadata.cs new file mode 100644 index 00000000..38c4020b --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/IBaseMetadata.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; + +namespace ModelContextProtocol.Protocol; + +/// Provides a base interface for metadata with name (identifier) and title (display name) properties. +public interface IBaseMetadata +{ + /// + /// Gets or sets the unique identifier for this item. + /// + [JsonPropertyName("name")] + string Name { get; set; } + + /// + /// Gets or sets a title. + /// + /// + /// 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, may be used for display (except for tools, where , if present, + /// should be given precedence over using ). + /// + [JsonPropertyName("title")] + string? Title { get; set; } +} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/Implementation.cs b/src/ModelContextProtocol.Core/Protocol/Implementation.cs index 7ebec95c..b13e43f2 100644 --- a/src/ModelContextProtocol.Core/Protocol/Implementation.cs +++ b/src/ModelContextProtocol.Core/Protocol/Implementation.cs @@ -17,17 +17,16 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class Implementation +public class Implementation : IBaseMetadata { - /// - /// Gets or sets the name of the implementation. - /// - /// - /// This is typically the name of the client or server library/application. - /// + /// [JsonPropertyName("name")] public required string Name { get; set; } + /// + [JsonPropertyName("title")] + public string? Title { get; set; } + /// /// Gets or sets the version of the implementation. /// diff --git a/src/ModelContextProtocol.Core/Protocol/Prompt.cs b/src/ModelContextProtocol.Core/Protocol/Prompt.cs index 8e2139f2..56753c3a 100644 --- a/src/ModelContextProtocol.Core/Protocol/Prompt.cs +++ b/src/ModelContextProtocol.Core/Protocol/Prompt.cs @@ -8,8 +8,16 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class Prompt +public class Prompt : IBaseMetadata { + /// + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// + [JsonPropertyName("title")] + public string? Title { get; set; } + /// /// Gets or sets a list of arguments that this prompt accepts for templating and customization. /// @@ -41,10 +49,4 @@ public class Prompt /// [JsonPropertyName("description")] public string? Description { get; set; } - - /// - /// Gets or sets the name of the prompt. - /// - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; } diff --git a/src/ModelContextProtocol.Core/Protocol/PromptArgument.cs b/src/ModelContextProtocol.Core/Protocol/PromptArgument.cs index a22bea94..944f4da3 100644 --- a/src/ModelContextProtocol.Core/Protocol/PromptArgument.cs +++ b/src/ModelContextProtocol.Core/Protocol/PromptArgument.cs @@ -15,13 +15,15 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class PromptArgument +public class PromptArgument : IBaseMetadata { - /// - /// Gets or sets the name of the argument used for referencing in prompt templates. - /// + /// [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + public required string Name { get; set; } + + /// + [JsonPropertyName("title")] + public string? Title { get; set; } /// /// Gets or sets a human-readable description of the argument's purpose and expected values. diff --git a/src/ModelContextProtocol.Core/Protocol/Reference.cs b/src/ModelContextProtocol.Core/Protocol/Reference.cs index 4bbff1f9..397bcb5b 100644 --- a/src/ModelContextProtocol.Core/Protocol/Reference.cs +++ b/src/ModelContextProtocol.Core/Protocol/Reference.cs @@ -1,5 +1,8 @@ using ModelContextProtocol.Client; +using System.ComponentModel; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -9,13 +12,6 @@ namespace ModelContextProtocol.Protocol; /// /// /// -/// A Reference object identifies either a resource or a prompt: -/// -/// -/// For resource references, set to "ref/resource" and provide the property. -/// For prompt references, set to "ref/prompt" and provide the property. -/// -/// /// References are commonly used with to request completion suggestions for arguments, /// and with other methods that need to reference resources or prompts. /// @@ -23,8 +19,11 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class Reference +[JsonConverter(typeof(Converter))] +public abstract class Reference { + private protected Reference() { } + /// /// Gets or sets the type of content. /// @@ -35,55 +34,154 @@ public class Reference public string Type { get; set; } = string.Empty; /// - /// Gets or sets the URI or URI template of the resource. + /// Provides a for . /// - [JsonPropertyName("uri")] - public string? Uri { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override Reference? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } - /// - /// Gets or sets the name of the prompt or prompt template. - /// - [JsonPropertyName("name")] - public string? Name { get; set; } + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } - /// - public override string ToString() => $"\"{Type}\": \"{Uri ?? Name}\""; + string? type = null; + string? name = null; + string? title = null; + string? uri = null; - /// - /// Validates the reference object to ensure it contains the required properties for its type. - /// - /// When this method returns false, contains a message explaining why validation failed; otherwise, null. - /// True if the reference is valid; otherwise, false. - /// - /// For "ref/resource" type, the property must not be null or empty. - /// For "ref/prompt" type, the property must not be null or empty. - /// - public bool Validate([NotNullWhen(false)] out string? validationMessage) - { - switch (Type) - { - case "ref/resource": - if (string.IsNullOrEmpty(Uri)) + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType != JsonTokenType.PropertyName) { - validationMessage = "Uri is required for ref/resource"; - return false; + continue; } - break; - case "ref/prompt": - if (string.IsNullOrEmpty(Name)) + string? propertyName = reader.GetString(); + bool success = reader.Read(); + Debug.Assert(success, "STJ must have buffered the entire object for us."); + + switch (propertyName) { - validationMessage = "Name is required for ref/prompt"; - return false; + case "type": + type = reader.GetString(); + break; + + case "name": + name = reader.GetString(); + break; + + case "uri": + uri = reader.GetString(); + break; + + default: + break; } - break; + } + + switch (type) + { + case "ref/prompt": + if (name is null) + { + throw new JsonException("Prompt references must have a 'name' property."); + } + + return new PromptReference() { Name = name, Title = title }; - default: - validationMessage = $"Unknown reference type: {Type}"; - return false; + case "ref/resource": + if (uri is null) + { + throw new JsonException("Resource references must have a 'uri' property."); + } + + return new ResourceTemplateReference() { Uri = uri }; + + default: + throw new JsonException($"Unknown content type: '{type}'"); + } } - validationMessage = null; - return true; + /// + public override void Write(Utf8JsonWriter writer, Reference value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartObject(); + + writer.WriteString("type", value.Type); + + switch (value) + { + case PromptReference pr: + writer.WriteString("name", pr.Name); + if (pr.Title is not null) + { + writer.WriteString("title", pr.Title); + } + break; + + case ResourceTemplateReference rtr: + writer.WriteString("uri", rtr.Uri); + break; + } + + writer.WriteEndObject(); + } } } + +/// +/// Represents a reference to a prompt, identified by its name. +/// +public sealed class PromptReference : Reference, IBaseMetadata +{ + /// + /// Initializes a new instance of the class. + /// + public PromptReference() => Type = "ref/prompt"; + + /// + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// + public override string ToString() => $"\"{Type}\": \"{Name}\""; +} + +/// +/// Represents a reference to a resource or resource template definition. +/// +public sealed class ResourceTemplateReference : Reference +{ + /// + /// Initializes a new instance of the class. + /// + public ResourceTemplateReference() => Type = "ref/resource"; + + /// + /// Gets or sets the URI or URI template of the resource. + /// + [JsonPropertyName("uri")] + [StringSyntax(StringSyntaxAttribute.Uri)] + public required string? Uri { get; set; } + + /// + public override string ToString() => $"\"{Type}\": \"{Uri}\""; +} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/Resource.cs b/src/ModelContextProtocol.Core/Protocol/Resource.cs index d9217b70..a2dfd8e5 100644 --- a/src/ModelContextProtocol.Core/Protocol/Resource.cs +++ b/src/ModelContextProtocol.Core/Protocol/Resource.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class Resource +public class Resource : IBaseMetadata { /// /// Gets or sets the URI of this resource. @@ -16,11 +16,13 @@ public class Resource [JsonPropertyName("uri")] public required string Uri { get; init; } - /// - /// Gets or sets a human-readable name for this resource. - /// + /// [JsonPropertyName("name")] - public required string Name { get; init; } + public required string Name { get; set; } + + /// + [JsonPropertyName("title")] + public string? Title { get; set; } /// /// Gets or sets a description of what this resource represents. diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs b/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs index 07bac92d..514eebb1 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs @@ -9,20 +9,22 @@ namespace ModelContextProtocol.Protocol; /// Resource templates provide metadata about resources available on the server, /// including how to construct URIs for those resources. /// -public class ResourceTemplate +public class ResourceTemplate : IBaseMetadata { + /// + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// + [JsonPropertyName("title")] + public string? Title { get; set; } + /// /// Gets or sets the URI template (according to RFC 6570) that can be used to construct resource URIs. /// [JsonPropertyName("uriTemplate")] public required string UriTemplate { get; init; } - /// - /// Gets or sets a human-readable name for this resource template. - /// - [JsonPropertyName("name")] - public required string Name { get; init; } - /// /// Gets or sets a description of what this resource template represents. /// @@ -85,6 +87,7 @@ public class ResourceTemplate { Uri = UriTemplate, Name = Name, + Title = Title, Description = Description, MimeType = MimeType, Annotations = Annotations, diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs index 09f1720e..52c12048 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -25,5 +26,6 @@ public class ResourceUpdatedNotificationParams /// The URI can use any protocol; it is up to the server how to interpret it. /// [JsonPropertyName("uri")] + [StringSyntax(StringSyntaxAttribute.Uri)] public string? Uri { get; init; } } diff --git a/src/ModelContextProtocol.Core/Protocol/SubscribeRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/SubscribeRequestParams.cs index 6716ba43..ac49512c 100644 --- a/src/ModelContextProtocol.Core/Protocol/SubscribeRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/SubscribeRequestParams.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -29,5 +30,6 @@ public class SubscribeRequestParams : RequestParams /// The URI can use any protocol; it is up to the server how to interpret it. /// [JsonPropertyName("uri")] + [StringSyntax(StringSyntaxAttribute.Uri)] public string? Uri { get; init; } } diff --git a/src/ModelContextProtocol.Core/Protocol/Tool.cs b/src/ModelContextProtocol.Core/Protocol/Tool.cs index 552f2538..95636392 100644 --- a/src/ModelContextProtocol.Core/Protocol/Tool.cs +++ b/src/ModelContextProtocol.Core/Protocol/Tool.cs @@ -6,14 +6,16 @@ namespace ModelContextProtocol.Protocol; /// /// Represents a tool that the server is capable of calling. /// -public class Tool +public class Tool : IBaseMetadata { - /// - /// Gets or sets the name of the tool. - /// + /// [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; + /// + [JsonPropertyName("title")] + public string? Title { get; set; } + /// /// Gets or sets a human-readable description of the tool. /// diff --git a/src/ModelContextProtocol.Core/Protocol/UnsubscribeRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/UnsubscribeRequestParams.cs index ac354128..62117f08 100644 --- a/src/ModelContextProtocol.Core/Protocol/UnsubscribeRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/UnsubscribeRequestParams.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -23,5 +24,6 @@ public class UnsubscribeRequestParams : RequestParams /// The URI of the resource to unsubscribe from. The URI can use any protocol; it is up to the server how to interpret it. /// [JsonPropertyName("uri")] + [StringSyntax(StringSyntaxAttribute.Uri)] public string? Uri { get; init; } } diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs index 9d18f3c2..a1948308 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs @@ -180,6 +180,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( Prompt prompt = new() { Name = options?.Name ?? function.Name, + Title = options?.Title, Description = options?.Description ?? function.Description, Arguments = args, }; @@ -194,6 +195,7 @@ private static McpServerPromptCreateOptions DeriveOptions(MethodInfo method, Mcp if (method.GetCustomAttribute() is { } promptAttr) { newOptions.Name ??= promptAttr.Name; + newOptions.Title ??= promptAttr.Title; } if (method.GetCustomAttribute() is { } descAttr) diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs index 76777963..2edd5e48 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs @@ -262,6 +262,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( { UriTemplate = options?.UriTemplate ?? DeriveUriTemplate(name, function), Name = name, + Title = options?.Title, Description = options?.Description, MimeType = options?.MimeType, }; @@ -277,6 +278,7 @@ private static McpServerResourceCreateOptions DeriveOptions(MemberInfo member, M { newOptions.UriTemplate ??= resourceAttr.UriTemplate; newOptions.Name ??= resourceAttr.Name; + newOptions.Title ??= resourceAttr.Title; newOptions.MimeType ??= resourceAttr.MimeType; } diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index 2146504b..7cdcce3c 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -190,13 +190,15 @@ options.Destructive is not null || options.OpenWorld is not null || options.ReadOnly is not null) { + tool.Title = options.Title; + tool.Annotations = new() { - Title = options?.Title, - IdempotentHint = options?.Idempotent, - DestructiveHint = options?.Destructive, - OpenWorldHint = options?.OpenWorld, - ReadOnlyHint = options?.ReadOnly, + Title = options.Title, + IdempotentHint = options.Idempotent, + DestructiveHint = options.Destructive, + OpenWorldHint = options.OpenWorld, + ReadOnlyHint = options.ReadOnly, }; } } diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs index 8e7fdf05..921ac12b 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs @@ -117,4 +117,7 @@ public McpServerPromptAttribute() /// Gets the name of the prompt. /// If , the method name will be used. public string? Name { get; set; } + + /// Gets or sets the title of the prompt. + public string? Title { get; set; } } diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs index 77584c48..95d712ff 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs @@ -38,6 +38,11 @@ public sealed class McpServerPromptCreateOptions /// public string? Name { get; set; } + /// + /// Gets or sets the title to use for the . + /// + public string? Title { get; set; } + /// /// Gets or set the description to use for the . /// @@ -71,6 +76,7 @@ internal McpServerPromptCreateOptions Clone() => { Services = Services, Name = Name, + Title = Title, Description = Description, SerializerOptions = SerializerOptions, SchemaCreateOptions = SchemaCreateOptions, diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptTypeAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptTypeAttribute.cs index 3f80b3c5..4fc38a58 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPromptTypeAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPromptTypeAttribute.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; - namespace ModelContextProtocol.Server; /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs index 9bdfa4d0..c75d1b10 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs @@ -130,6 +130,9 @@ public McpServerResourceAttribute() /// If , the method name will be used. public string? Name { get; set; } + /// Gets or sets the title of the resource. + public string? Title { get; set; } + /// Gets or sets the MIME (media) type of the resource. public string? MimeType { get; set; } } diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs index 191ec978..24051a7f 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs @@ -48,6 +48,11 @@ public sealed class McpServerResourceCreateOptions /// public string? Name { get; set; } + /// + /// Gets or sets the title to use for the . + /// + public string? Title { get; set; } + /// /// Gets or set the description to use for the . /// @@ -87,6 +92,7 @@ internal McpServerResourceCreateOptions Clone() => Services = Services, UriTemplate = UriTemplate, Name = Name, + Title = Title, Description = Description, MimeType = MimeType, SerializerOptions = SerializerOptions, diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceTypeAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceTypeAttribute.cs index e287765c..4d549afb 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResourceTypeAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResourceTypeAttribute.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; - namespace ModelContextProtocol.Server; /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolTypeAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerToolTypeAttribute.cs index ee13ddc6..8eff1254 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolTypeAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolTypeAttribute.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; - namespace ModelContextProtocol.Server; /// diff --git a/src/ModelContextProtocol.Core/Server/RequestContext.cs b/src/ModelContextProtocol.Core/Server/RequestContext.cs index ee8d4a34..b0ea9d99 100644 --- a/src/ModelContextProtocol.Core/Server/RequestContext.cs +++ b/src/ModelContextProtocol.Core/Server/RequestContext.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; - namespace ModelContextProtocol.Server; /// diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs index b8d31c37..9325fdc1 100644 --- a/tests/ModelContextProtocol.TestServer/Program.cs +++ b/tests/ModelContextProtocol.TestServer/Program.cs @@ -493,29 +493,29 @@ private static CompletionsCapability ConfigureCompletions() Func, CancellationToken, ValueTask> handler = async (request, cancellationToken) => { - if (request.Params?.Ref?.Type == "ref/resource") + string[]? values; + switch (request.Params?.Ref) { - var resourceId = request.Params?.Ref?.Uri?.Split('/').LastOrDefault(); - if (string.IsNullOrEmpty(resourceId)) - return new CompleteResult() { Completion = new() { Values = [] } }; + case ResourceTemplateReference rtr: + var resourceId = rtr.Uri?.Split('/').LastOrDefault(); + if (string.IsNullOrEmpty(resourceId)) + return new CompleteResult() { Completion = new() { Values = [] } }; - // Filter resource IDs that start with the input value - var values = sampleResourceIds.Where(id => id.StartsWith(request.Params!.Argument.Value)).ToArray(); - return new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } }; + // Filter resource IDs that start with the input value + values = sampleResourceIds.Where(id => id.StartsWith(request.Params!.Argument.Value)).ToArray(); + return new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } }; - } + case PromptReference pr: + // Handle completion for prompt arguments + if (!exampleCompletions.TryGetValue(request.Params.Argument.Name, out var completions)) + return new CompleteResult() { Completion = new() { Values = [] } }; - if (request.Params?.Ref?.Type == "ref/prompt") - { - // Handle completion for prompt arguments - if (!exampleCompletions.TryGetValue(request.Params.Argument.Name, out var completions)) - return new CompleteResult() { Completion = new() { Values = [] } }; + values = completions.Where(value => value.StartsWith(request.Params.Argument.Value)).ToArray(); + return new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } }; - var values = completions.Where(value => value.StartsWith(request.Params.Argument.Value)).ToArray(); - return new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } }; + default: + throw new McpException($"Unknown reference type: '{request.Params?.Ref.Type}'", McpErrorCode.InvalidParams); } - - throw new McpException($"Unknown reference type: '{request.Params?.Ref.Type}'", McpErrorCode.InvalidParams); }; return new() { CompleteHandler = handler }; diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index f909db18..05492434 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -332,11 +332,8 @@ public async Task Complete_Stdio_ResourceTemplateReference(string clientId) // act await using var client = await _fixture.CreateClientAsync(clientId); - var result = await client.CompleteAsync(new Reference - { - Type = "ref/resource", - Uri = "test://static/resource/1" - }, + var result = await client.CompleteAsync( + new ResourceTemplateReference { Uri = "test://static/resource/1" }, "argument_name", "1", TestContext.Current.CancellationToken ); @@ -354,11 +351,8 @@ public async Task Complete_Stdio_PromptReference(string clientId) // act await using var client = await _fixture.CreateClientAsync(clientId); - var result = await client.CompleteAsync(new Reference - { - Type = "ref/prompt", - Name = "irrelevant" - }, + var result = await client.CompleteAsync( + new PromptReference { Name = "irrelevant" }, argumentName: "style", argumentValue: "fo", TestContext.Current.CancellationToken ); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index a4747185..f98fa1ad 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -162,6 +162,20 @@ public async Task Can_Be_Notified_Of_Prompt_Changes() Assert.DoesNotContain(prompts, t => t.Name == "NewPrompt"); } + [Fact] + public async Task TitleAttributeProperty_PropagatedToTitle() + { + await using IMcpClient client = await CreateMcpClientForServer(); + + var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(prompts); + Assert.NotEmpty(prompts); + + McpClientPrompt prompt = prompts.First(t => t.Name == nameof(SimplePrompts.ReturnsString)); + + Assert.Equal("This is a title", prompt.Title); + } + [Fact] public async Task Throws_When_Prompt_Fails() { @@ -263,7 +277,7 @@ public static ChatMessage[] ReturnsChatMessages([Description("The first paramete public static ChatMessage[] ThrowsException([Description("The first parameter")] string message) => throw new FormatException("uh oh"); - [McpServerPrompt, Description("Returns chat messages")] + [McpServerPrompt(Title = "This is a title"), Description("Returns chat messages")] public string ReturnsString([Description("The first parameter")] string message) => $"The prompt is: {message}. The id is {id}."; } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs index e8576697..ef0eebe8 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs @@ -196,6 +196,24 @@ public async Task Can_Be_Notified_Of_Resource_Changes() Assert.DoesNotContain(resources, t => t.Name == "NewResource"); } + [Fact] + public async Task TitleAttributeProperty_PropagatedToTitle() + { + await using IMcpClient client = await CreateMcpClientForServer(); + + var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(resources); + Assert.NotEmpty(resources); + McpClientResource resource = resources.First(t => t.Name == nameof(SimpleResources.SomeNeatDirectResource)); + Assert.Equal("This is a title", resource.Title); + + var resourceTemplates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(resourceTemplates); + Assert.NotEmpty(resourceTemplates); + McpClientResourceTemplate resourceTemplate = resourceTemplates.First(t => t.Name == nameof(SimpleResources.SomeNeatTemplatedResource)); + Assert.Equal("This is another title", resourceTemplate.Title); + } + [Fact] public async Task Throws_When_Resource_Fails() { @@ -273,10 +291,10 @@ public void Register_Resources_From_Multiple_Sources() [McpServerResourceType] public sealed class SimpleResources { - [McpServerResource, Description("Some neat direct resource")] + [McpServerResource(Title = "This is a title"), Description("Some neat direct resource")] public static string SomeNeatDirectResource() => "This is a neat resource"; - [McpServerResource, Description("Some neat resource with parameters")] + [McpServerResource(Title = "This is another title"), Description("Some neat resource with parameters")] public static string SomeNeatTemplatedResource(string name) => $"This is a neat resource with parameters: {name}"; [McpServerResource] diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index 2a517919..b98605d7 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -559,6 +559,7 @@ public void Create_ExtractsToolAnnotations_AllSet() var annotations = tool.ProtocolTool.Annotations; Assert.NotNull(annotations); Assert.Equal("Return An Integer", annotations.Title); + Assert.Equal("Return An Integer", tool.ProtocolTool.Title); Assert.False(annotations.DestructiveHint); Assert.True(annotations.IdempotentHint); Assert.False(annotations.OpenWorldHint); @@ -581,6 +582,22 @@ public void Create_ExtractsToolAnnotations_SomeSet() Assert.Null(annotations.ReadOnlyHint); } + [Fact] + public async Task TitleAttributeProperty_PropagatedToTitle() + { + await using IMcpClient client = await CreateMcpClientForServer(); + + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(tools); + Assert.NotEmpty(tools); + + McpClientTool tool = tools.First(t => t.Name == nameof(EchoTool.EchoComplex)); + + Assert.Equal("This is a title", tool.Title); + Assert.Equal("This is a title", tool.ProtocolTool.Title); + Assert.Equal("This is a title", tool.ProtocolTool.Annotations?.Title); + } + [Fact] public async Task HandlesIProgressParameter() { @@ -722,7 +739,7 @@ public static int ReturnCancellationToken(CancellationToken cancellationToken) return cancellationToken.GetHashCode(); } - [McpServerTool] + [McpServerTool(Title = "This is a title")] public static string EchoComplex(ComplexObject complex) { return complex.Name!; diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsTransportsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsTransportsTests.cs index f0dd2116..a52746b8 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsTransportsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsTransportsTests.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Hosting; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; -using Moq; using System.IO.Pipelines; namespace ModelContextProtocol.Tests.Configuration; diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs index 42e5e505..cbe44da1 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Options; -using ModelContextProtocol.Protocol; +using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using ModelContextProtocol.Tests.Utils; using System.IO.Pipelines; From 33e438ea5f293b82c39202a92362dad4f88d3b36 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 13 Jun 2025 22:20:54 -0400 Subject: [PATCH 3/4] Add _meta to lots of types --- .../EverythingServer/Tools/LongRunningTool.cs | 2 +- .../Client/McpClient.cs | 11 +-- .../Client/McpClientExtensions.cs | 6 +- .../McpEndpointExtensions.cs | 4 +- .../McpJsonUtilities.cs | 18 +++-- src/ModelContextProtocol.Core/McpSession.cs | 10 +-- .../Protocol/CallToolResult.cs | 2 +- ...tion.cs => CancelledNotificationParams.cs} | 2 +- .../Protocol/CompleteResult.cs | 2 +- .../Protocol/ContentBlock.cs | 69 ++++++++++++++++++- .../Protocol/CreateMessageResult.cs | 2 +- .../Protocol/ElicitResult.cs | 2 +- .../Protocol/EmptyResult.cs | 2 +- .../Protocol/GetPromptResult.cs | 2 +- .../Protocol/InitializeResult.cs | 2 +- .../Protocol/InitializedNotificationParams.cs | 10 +++ .../Protocol/ListRootsResult.cs | 11 +-- .../LoggingMessageNotificationParams.cs | 2 +- .../Protocol/NotificationMethods.cs | 2 +- .../Protocol/NotificationParams.cs | 19 +++++ .../Protocol/PaginatedRequest.cs | 2 +- .../Protocol/PaginatedResult.cs | 2 +- .../Protocol/PingResult.cs | 2 +- ...ation.cs => ProgressNotificationParams.cs} | 32 ++++++--- .../Protocol/Prompt.cs | 32 ++++++--- .../PromptListChangedNotification .cs | 15 ++++ .../Protocol/ReadResourceResult.cs | 2 +- .../Protocol/RequestParams.cs | 49 +++++++++++-- .../Protocol/Resource.cs | 32 ++++++--- .../Protocol/ResourceContents.cs | 27 +++++++- .../ResourceListChangedNotificationParams.cs | 15 ++++ .../Protocol/ResourceTemplate.cs | 11 +++ .../ResourceUpdatedNotificationParams.cs | 2 +- .../Protocol/Result.cs | 19 +++++ .../Protocol/Root.cs | 2 +- .../RootsListChangedNotificationParams.cs | 15 ++++ .../Protocol/Tool.cs | 10 +++ .../ToolListChangedNotificationParams.cs | 15 ++++ .../Server/AIFunctionMcpServerPrompt.cs | 2 +- .../Server/AIFunctionMcpServerResource.cs | 2 +- .../Server/AIFunctionMcpServerTool.cs | 2 +- .../Server/McpServerPrompt.cs | 2 +- .../Server/McpServerPromptAttribute.cs | 2 +- .../Server/McpServerResource.cs | 2 +- .../Server/McpServerResourceAttribute.cs | 2 +- .../Server/McpServerTool.cs | 2 +- .../Server/McpServerToolAttribute.cs | 2 +- .../StreamableHttpServerConformanceTests.cs | 2 +- .../Client/McpClientExtensionsTests.cs | 4 -- .../ClientIntegrationTests.cs | 2 +- .../McpServerBuilderExtensionsToolsTests.cs | 10 +-- .../Server/McpServerTests.cs | 4 +- 52 files changed, 399 insertions(+), 105 deletions(-) rename src/ModelContextProtocol.Core/Protocol/{CancelledNotification.cs => CancelledNotificationParams.cs} (93%) create mode 100644 src/ModelContextProtocol.Core/Protocol/InitializedNotificationParams.cs create mode 100644 src/ModelContextProtocol.Core/Protocol/NotificationParams.cs rename src/ModelContextProtocol.Core/Protocol/{ProgressNotification.cs => ProgressNotificationParams.cs} (81%) create mode 100644 src/ModelContextProtocol.Core/Protocol/PromptListChangedNotification .cs create mode 100644 src/ModelContextProtocol.Core/Protocol/ResourceListChangedNotificationParams.cs create mode 100644 src/ModelContextProtocol.Core/Protocol/Result.cs create mode 100644 src/ModelContextProtocol.Core/Protocol/RootsListChangedNotificationParams.cs create mode 100644 src/ModelContextProtocol.Core/Protocol/ToolListChangedNotificationParams.cs diff --git a/samples/EverythingServer/Tools/LongRunningTool.cs b/samples/EverythingServer/Tools/LongRunningTool.cs index 8ba8b047..27f6ac20 100644 --- a/samples/EverythingServer/Tools/LongRunningTool.cs +++ b/samples/EverythingServer/Tools/LongRunningTool.cs @@ -15,7 +15,7 @@ public static async Task LongRunningOperation( int duration = 10, int steps = 5) { - var progressToken = context.Params?.Meta?.ProgressToken; + var progressToken = context.Params?.ProgressToken; var stepDuration = duration / steps; for (int i = 1; i <= steps + 1; i++) diff --git a/src/ModelContextProtocol.Core/Client/McpClient.cs b/src/ModelContextProtocol.Core/Client/McpClient.cs index c4f23238..dd8c7fe0 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.cs @@ -57,7 +57,7 @@ public McpClient(IClientTransport clientTransport, McpClientOptions? options, IL RequestMethods.SamplingCreateMessage, (request, _, cancellationToken) => samplingHandler( request, - request?.Meta?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance, + request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance, cancellationToken), McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, McpJsonUtilities.JsonContext.Default.CreateMessageResult); @@ -179,9 +179,12 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) } // Send initialized notification - await SendMessageAsync( - new JsonRpcNotification { Method = NotificationMethods.InitializedNotification }, - initializationCts.Token).ConfigureAwait(false); + await this.SendNotificationAsync( + NotificationMethods.InitializedNotification, + new InitializedNotificationParams(), + McpJsonUtilities.JsonContext.Default.InitializedNotificationParams, + cancellationToken: initializationCts.Token).ConfigureAwait(false); + } catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) { diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index df7ca2d6..bc22275d 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -866,7 +866,7 @@ static async ValueTask SendRequestWithProgressAsync( await using var _ = client.RegisterNotificationHandler(NotificationMethods.ProgressNotification, (notification, cancellationToken) => { - if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotification) is { } pn && + if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams) is { } pn && pn.ProgressToken == progressToken) { progress.Report(pn.Progress); @@ -881,7 +881,7 @@ static async ValueTask SendRequestWithProgressAsync( { Name = toolName, Arguments = ToArgumentsDictionary(arguments, serializerOptions), - Meta = new() { ProgressToken = progressToken }, + ProgressToken = progressToken, }, McpJsonUtilities.JsonContext.Default.CallToolRequestParams, McpJsonUtilities.JsonContext.Default.CallToolResult, @@ -991,7 +991,7 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat Throw.IfNull(requestParams); var (messages, options) = requestParams.ToChatClientArguments(); - var progressToken = requestParams.Meta?.ProgressToken; + var progressToken = requestParams.ProgressToken; List updates = []; await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) diff --git a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs index 300433e0..4e4abe5c 100644 --- a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs +++ b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs @@ -201,12 +201,12 @@ public static Task NotifyProgressAsync( return endpoint.SendNotificationAsync( NotificationMethods.ProgressNotification, - new ProgressNotification + new ProgressNotificationParams { ProgressToken = progressToken, Progress = progress, }, - McpJsonUtilities.JsonContext.Default.ProgressNotification, + McpJsonUtilities.JsonContext.Default.ProgressNotificationParams, cancellationToken); } } diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs index 1fd3ce4c..7d0cc8b0 100644 --- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs +++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs @@ -96,10 +96,20 @@ internal static bool IsValidMcpToolSchema(JsonElement element) [JsonSerializable(typeof(JsonRpcResponse))] [JsonSerializable(typeof(JsonRpcError))] + // MCP Notification Params + [JsonSerializable(typeof(CancelledNotificationParams))] + [JsonSerializable(typeof(InitializedNotificationParams))] + [JsonSerializable(typeof(LoggingMessageNotificationParams))] + [JsonSerializable(typeof(ProgressNotificationParams))] + [JsonSerializable(typeof(PromptListChangedNotificationParams))] + [JsonSerializable(typeof(ResourceListChangedNotificationParams))] + [JsonSerializable(typeof(ResourceUpdatedNotificationParams))] + [JsonSerializable(typeof(RootsListChangedNotificationParams))] + [JsonSerializable(typeof(ToolListChangedNotificationParams))] + // MCP Request Params / Results [JsonSerializable(typeof(CallToolRequestParams))] [JsonSerializable(typeof(CallToolResult))] - [JsonSerializable(typeof(CancelledNotification))] [JsonSerializable(typeof(CompleteRequestParams))] [JsonSerializable(typeof(CompleteResult))] [JsonSerializable(typeof(CreateMessageRequestParams))] @@ -121,16 +131,16 @@ internal static bool IsValidMcpToolSchema(JsonElement element) [JsonSerializable(typeof(ListRootsResult))] [JsonSerializable(typeof(ListToolsRequestParams))] [JsonSerializable(typeof(ListToolsResult))] - [JsonSerializable(typeof(LoggingMessageNotificationParams))] [JsonSerializable(typeof(PingResult))] - [JsonSerializable(typeof(ProgressNotification))] [JsonSerializable(typeof(ReadResourceRequestParams))] [JsonSerializable(typeof(ReadResourceResult))] - [JsonSerializable(typeof(ResourceUpdatedNotificationParams))] [JsonSerializable(typeof(SetLevelRequestParams))] [JsonSerializable(typeof(SubscribeRequestParams))] [JsonSerializable(typeof(UnsubscribeRequestParams))] + + // Other MCP Types [JsonSerializable(typeof(IReadOnlyDictionary))] + [JsonSerializable(typeof(ProgressToken))] [JsonSerializable(typeof(PromptMessage[]))] // Primitive types for use in consuming AIFunctions diff --git a/src/ModelContextProtocol.Core/McpSession.cs b/src/ModelContextProtocol.Core/McpSession.cs index 8a0ae6b1..225971fa 100644 --- a/src/ModelContextProtocol.Core/McpSession.cs +++ b/src/ModelContextProtocol.Core/McpSession.cs @@ -273,7 +273,7 @@ private async Task HandleNotification(JsonRpcNotification notification, Cancella { try { - if (GetCancelledNotificationParams(notification.Params) is CancelledNotification cn && + if (GetCancelledNotificationParams(notification.Params) is CancelledNotificationParams cn && _handlingRequests.TryGetValue(cn.RequestId, out var cts)) { await cts.CancelAsync().ConfigureAwait(false); @@ -337,7 +337,7 @@ private CancellationTokenRegistration RegisterCancellation(CancellationToken can _ = state.Item1.SendMessageAsync(new JsonRpcNotification { Method = NotificationMethods.CancelledNotification, - Params = JsonSerializer.SerializeToNode(new CancelledNotification { RequestId = state.Item2.Id }, McpJsonUtilities.JsonContext.Default.CancelledNotification), + Params = JsonSerializer.SerializeToNode(new CancelledNotificationParams { RequestId = state.Item2.Id }, McpJsonUtilities.JsonContext.Default.CancelledNotificationParams), RelatedTransport = state.Item2.RelatedTransport, }); }, Tuple.Create(this, request)); @@ -495,7 +495,7 @@ public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken can // server won't be sending a response, or per the specification, the response should be ignored. There are inherent // race conditions here, so it's possible and allowed for the operation to complete before we get to this point. if (message is JsonRpcNotification { Method: NotificationMethods.CancelledNotification } notification && - GetCancelledNotificationParams(notification.Params) is CancelledNotification cn && + GetCancelledNotificationParams(notification.Params) is CancelledNotificationParams cn && _pendingRequests.TryRemove(cn.RequestId, out var tcs)) { tcs.TrySetCanceled(default); @@ -518,11 +518,11 @@ public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken can private Task SendToRelatedTransportAsync(JsonRpcMessage message, CancellationToken cancellationToken) => (message.RelatedTransport ?? _transport).SendMessageAsync(message, cancellationToken); - private static CancelledNotification? GetCancelledNotificationParams(JsonNode? notificationParams) + private static CancelledNotificationParams? GetCancelledNotificationParams(JsonNode? notificationParams) { try { - return JsonSerializer.Deserialize(notificationParams, McpJsonUtilities.JsonContext.Default.CancelledNotification); + return JsonSerializer.Deserialize(notificationParams, McpJsonUtilities.JsonContext.Default.CancelledNotificationParams); } catch { diff --git a/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs b/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs index b23d4081..43eb353f 100644 --- a/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs @@ -20,7 +20,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class CallToolResult +public class CallToolResult : Result { /// /// Gets or sets the response content from the tool call. diff --git a/src/ModelContextProtocol.Core/Protocol/CancelledNotification.cs b/src/ModelContextProtocol.Core/Protocol/CancelledNotificationParams.cs similarity index 93% rename from src/ModelContextProtocol.Core/Protocol/CancelledNotification.cs rename to src/ModelContextProtocol.Core/Protocol/CancelledNotificationParams.cs index 546e03aa..8b59e7e2 100644 --- a/src/ModelContextProtocol.Core/Protocol/CancelledNotification.cs +++ b/src/ModelContextProtocol.Core/Protocol/CancelledNotificationParams.cs @@ -11,7 +11,7 @@ namespace ModelContextProtocol.Protocol; /// method identifier. When a client sends this notification, the server should attempt to /// cancel any ongoing operations associated with the specified request ID. /// -public sealed class CancelledNotification +public sealed class CancelledNotificationParams : NotificationParams { /// /// Gets or sets the ID of the request to cancel. diff --git a/src/ModelContextProtocol.Core/Protocol/CompleteResult.cs b/src/ModelContextProtocol.Core/Protocol/CompleteResult.cs index dd28de55..4ca366bc 100644 --- a/src/ModelContextProtocol.Core/Protocol/CompleteResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/CompleteResult.cs @@ -23,7 +23,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class CompleteResult +public class CompleteResult : Result { /// /// Gets or sets the completion object containing the suggested values and pagination information. diff --git a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs index 6e5ad1d9..4440efa7 100644 --- a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs +++ b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs @@ -1,6 +1,8 @@ +using Microsoft.Extensions.AI; using System.ComponentModel; using System.Diagnostics; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -78,6 +80,7 @@ public class Converter : JsonConverter long? size = null; ResourceContents? resource = null; Annotations? annotations = null; + JsonObject? meta = null; while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { @@ -128,6 +131,10 @@ public class Converter : JsonConverter annotations = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.Annotations); break; + case "_meta": + meta = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonObject); + break; + default: break; } @@ -140,6 +147,7 @@ public class Converter : JsonConverter { Text = text ?? throw new JsonException("Text contents must be provided for 'text' type."), Annotations = annotations, + Meta = meta, }; case "image": @@ -148,6 +156,7 @@ public class Converter : JsonConverter 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."), Annotations = annotations, + Meta = meta, }; case "audio": @@ -156,13 +165,15 @@ public class Converter : JsonConverter 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."), Annotations = annotations, + Meta = meta, }; case "resource": return new EmbeddedResourceBlock() { Resource = resource ?? throw new JsonException("Resource contents must be provided for 'resource' type."), - Annotations = annotations + Annotations = annotations, + Meta = meta, }; case "resource_link": @@ -198,21 +209,41 @@ public override void Write(Utf8JsonWriter writer, ContentBlock value, JsonSerial { case TextContentBlock textContent: writer.WriteString("text", textContent.Text); + if (textContent.Meta is not null) + { + writer.WritePropertyName("_meta"); + JsonSerializer.Serialize(writer, textContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); + } break; case ImageContentBlock imageContent: writer.WriteString("data", imageContent.Data); writer.WriteString("mimeType", imageContent.MimeType); + if (imageContent.Meta is not null) + { + writer.WritePropertyName("_meta"); + JsonSerializer.Serialize(writer, imageContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); + } break; case AudioContentBlock audioContent: writer.WriteString("data", audioContent.Data); writer.WriteString("mimeType", audioContent.MimeType); + if (audioContent.Meta is not null) + { + writer.WritePropertyName("_meta"); + JsonSerializer.Serialize(writer, audioContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); + } break; case EmbeddedResourceBlock embeddedResource: writer.WritePropertyName("resource"); JsonSerializer.Serialize(writer, embeddedResource.Resource, McpJsonUtilities.JsonContext.Default.ResourceContents); + if (embeddedResource.Meta is not null) + { + writer.WritePropertyName("_meta"); + JsonSerializer.Serialize(writer, embeddedResource.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); + } break; case ResourceLinkBlock resourceLink: @@ -255,6 +286,15 @@ public sealed class TextContentBlock : ContentBlock /// [JsonPropertyName("text")] public required string Text { 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; } } /// Represents an image provided to or from an LLM. @@ -279,6 +319,15 @@ public sealed class ImageContentBlock : ContentBlock /// [JsonPropertyName("mimeType")] public required string MimeType { 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; } } /// Represents audio provided to or from an LLM. @@ -303,6 +352,15 @@ public sealed class AudioContentBlock : ContentBlock /// [JsonPropertyName("mimeType")] public required string MimeType { 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; } } /// Represents the contents of a resource, embedded into a prompt or tool call result. @@ -326,6 +384,15 @@ public sealed class EmbeddedResourceBlock : ContentBlock /// [JsonPropertyName("resource")] public required ResourceContents Resource { 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; } } /// Represents a resource that the server is capable of reading, included in a prompt or tool call result. diff --git a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs index b18e0796..d47c4e18 100644 --- a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class CreateMessageResult +public class CreateMessageResult : Result { /// /// Gets or sets the content of the message. diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs index 4de7ded0..3b34cad2 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol; /// /// Represents the client's response to an elicitation request. /// -public class ElicitResult +public class ElicitResult : Result { /// /// Gets or sets the user action in response to the elicitation. diff --git a/src/ModelContextProtocol.Core/Protocol/EmptyResult.cs b/src/ModelContextProtocol.Core/Protocol/EmptyResult.cs index 2c9bfc09..fb86a2c2 100644 --- a/src/ModelContextProtocol.Core/Protocol/EmptyResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/EmptyResult.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol; /// Represents an empty result object for operations that need to indicate successful completion /// but don't need to return any specific data. /// -public class EmptyResult +public class EmptyResult : Result { [JsonIgnore] internal static EmptyResult Instance { get; } = new(); diff --git a/src/ModelContextProtocol.Core/Protocol/GetPromptResult.cs b/src/ModelContextProtocol.Core/Protocol/GetPromptResult.cs index bf6f75a1..bd79fd54 100644 --- a/src/ModelContextProtocol.Core/Protocol/GetPromptResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/GetPromptResult.cs @@ -15,7 +15,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class GetPromptResult +public class GetPromptResult : Result { /// /// Gets or sets an optional description for the prompt. diff --git a/src/ModelContextProtocol.Core/Protocol/InitializeResult.cs b/src/ModelContextProtocol.Core/Protocol/InitializeResult.cs index 58115272..24d00ed2 100644 --- a/src/ModelContextProtocol.Core/Protocol/InitializeResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/InitializeResult.cs @@ -19,7 +19,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class InitializeResult +public class InitializeResult : Result { /// /// Gets or sets the version of the Model Context Protocol that the server will use for this session. diff --git a/src/ModelContextProtocol.Core/Protocol/InitializedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/InitializedNotificationParams.cs new file mode 100644 index 00000000..7375e2d4 --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/InitializedNotificationParams.cs @@ -0,0 +1,10 @@ +namespace ModelContextProtocol.Protocol; + +/// +/// Represents the parameters used with a +/// sent from the client to the server after initialization has finished. +/// +/// +/// See the schema for details. +/// +public class InitializedNotificationParams : NotificationParams; diff --git a/src/ModelContextProtocol.Core/Protocol/ListRootsResult.cs b/src/ModelContextProtocol.Core/Protocol/ListRootsResult.cs index aad315b2..27ce1ad9 100644 --- a/src/ModelContextProtocol.Core/Protocol/ListRootsResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ListRootsResult.cs @@ -16,17 +16,8 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ListRootsResult +public class ListRootsResult : Result { - /// - /// Gets or sets additional metadata for the result. - /// - /// - /// This property is reserved by the protocol for future use. - /// - [JsonPropertyName("meta")] - public JsonElement? Meta { get; init; } - /// /// Gets or sets the list of root URIs provided by the client. /// diff --git a/src/ModelContextProtocol.Core/Protocol/LoggingMessageNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/LoggingMessageNotificationParams.cs index 12b6006c..bf2236b5 100644 --- a/src/ModelContextProtocol.Core/Protocol/LoggingMessageNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/LoggingMessageNotificationParams.cs @@ -20,7 +20,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class LoggingMessageNotificationParams +public class LoggingMessageNotificationParams : NotificationParams { /// /// Gets or sets the severity of this log message. diff --git a/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs b/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs index 70696898..30b7d68a 100644 --- a/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs +++ b/src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs @@ -62,7 +62,7 @@ public static class NotificationMethods /// method to get the updated list of roots from the client. /// /// - public const string RootsUpdatedNotification = "notifications/roots/list_changed"; + public const string RootsListChangedNotification = "notifications/roots/list_changed"; /// /// The name of the notification sent by the server when a log message is generated. diff --git a/src/ModelContextProtocol.Core/Protocol/NotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/NotificationParams.cs new file mode 100644 index 00000000..29668863 --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/NotificationParams.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace ModelContextProtocol.Protocol; + +/// +/// Provides a base class for notification parameters. +/// +public abstract class NotificationParams +{ + /// + /// 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; } +} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/PaginatedRequest.cs b/src/ModelContextProtocol.Core/Protocol/PaginatedRequest.cs index 3e25b3b6..1ab08d27 100644 --- a/src/ModelContextProtocol.Core/Protocol/PaginatedRequest.cs +++ b/src/ModelContextProtocol.Core/Protocol/PaginatedRequest.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details /// -public class PaginatedRequestParams : RequestParams +public abstract class PaginatedRequestParams : RequestParams { /// /// Gets or sets an opaque token representing the current pagination position. diff --git a/src/ModelContextProtocol.Core/Protocol/PaginatedResult.cs b/src/ModelContextProtocol.Core/Protocol/PaginatedResult.cs index b1eb67bb..1512dc37 100644 --- a/src/ModelContextProtocol.Core/Protocol/PaginatedResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/PaginatedResult.cs @@ -15,7 +15,7 @@ namespace ModelContextProtocol.Protocol; /// set of results. /// /// -public class PaginatedResult +public abstract class PaginatedResult : Result { /// /// Gets or sets an opaque token representing the pagination position after the last returned result. diff --git a/src/ModelContextProtocol.Core/Protocol/PingResult.cs b/src/ModelContextProtocol.Core/Protocol/PingResult.cs index cc2bc5c3..8dfef3f5 100644 --- a/src/ModelContextProtocol.Core/Protocol/PingResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/PingResult.cs @@ -14,4 +14,4 @@ namespace ModelContextProtocol.Protocol; /// is still responsive. /// /// -public class PingResult; \ No newline at end of file +public class PingResult : Result; \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/ProgressNotification.cs b/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs similarity index 81% rename from src/ModelContextProtocol.Core/Protocol/ProgressNotification.cs rename to src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs index 59643e4b..059aed57 100644 --- a/src/ModelContextProtocol.Core/Protocol/ProgressNotification.cs +++ b/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs @@ -1,5 +1,7 @@ +using Microsoft.Extensions.Logging.Abstractions; using System.ComponentModel; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -11,7 +13,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for more details. /// [JsonConverter(typeof(Converter))] -public class ProgressNotification +public class ProgressNotificationParams : NotificationParams { /// /// Gets or sets the progress token which was given in the initial request, used to associate this notification with @@ -38,19 +40,20 @@ public class ProgressNotification public required ProgressNotificationValue Progress { get; init; } /// - /// Provides a for . + /// Provides a for . /// [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override ProgressNotification? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override ProgressNotificationParams? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { ProgressToken? progressToken = null; float? progress = null; float? total = null; string? message = null; - + JsonObject? meta = null; + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { if (reader.TokenType == JsonTokenType.PropertyName) @@ -74,6 +77,10 @@ public sealed class Converter : JsonConverter case "message": message = reader.GetString(); break; + + case "_meta": + meta = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonObject); + break; } } } @@ -88,20 +95,21 @@ public sealed class Converter : JsonConverter throw new JsonException("Missing required property 'progressToken'."); } - return new ProgressNotification + return new ProgressNotificationParams { ProgressToken = progressToken.GetValueOrDefault(), Progress = new ProgressNotificationValue() { Progress = progress.GetValueOrDefault(), Total = total, - Message = message - } + Message = message, + }, + Meta = meta, }; } /// - public override void Write(Utf8JsonWriter writer, ProgressNotification value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ProgressNotificationParams value, JsonSerializerOptions options) { writer.WriteStartObject(); @@ -120,6 +128,12 @@ public override void Write(Utf8JsonWriter writer, ProgressNotification value, Js writer.WriteString("message", message); } + if (value.Meta is { } meta) + { + writer.WritePropertyName("_meta"); + JsonSerializer.Serialize(writer, meta, McpJsonUtilities.JsonContext.Default.JsonObject); + } + writer.WriteEndObject(); } } diff --git a/src/ModelContextProtocol.Core/Protocol/Prompt.cs b/src/ModelContextProtocol.Core/Protocol/Prompt.cs index 56753c3a..580ac705 100644 --- a/src/ModelContextProtocol.Core/Protocol/Prompt.cs +++ b/src/ModelContextProtocol.Core/Protocol/Prompt.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -18,6 +19,22 @@ public class Prompt : IBaseMetadata [JsonPropertyName("title")] public string? Title { get; set; } + /// + /// Gets or sets an optional description of what this prompt provides. + /// + /// + /// + /// This description helps developers understand the purpose and use cases for the prompt. + /// It should explain what the prompt is designed to accomplish and any important context. + /// + /// + /// The description is typically used in documentation, UI displays, and for providing context + /// to client applications that may need to choose between multiple available prompts. + /// + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + /// /// Gets or sets a list of arguments that this prompt accepts for templating and customization. /// @@ -35,18 +52,11 @@ public class Prompt : IBaseMetadata public List? Arguments { get; set; } /// - /// Gets or sets an optional description of what this prompt provides. + /// Gets or sets metadata reserved by MCP for protocol-level metadata. /// /// - /// - /// This description helps developers understand the purpose and use cases for the prompt. - /// It should explain what the prompt is designed to accomplish and any important context. - /// - /// - /// The description is typically used in documentation, UI displays, and for providing context - /// to client applications that may need to choose between multiple available prompts. - /// + /// Implementations must not make assumptions about its contents. /// - [JsonPropertyName("description")] - public string? Description { get; set; } + [JsonPropertyName("_meta")] + public JsonObject? Meta { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/PromptListChangedNotification .cs b/src/ModelContextProtocol.Core/Protocol/PromptListChangedNotification .cs new file mode 100644 index 00000000..7b3ff7f5 --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/PromptListChangedNotification .cs @@ -0,0 +1,15 @@ +namespace ModelContextProtocol.Protocol; + +/// +/// Represents the parameters used with a +/// notification from the server to the client, informing it that the list of prompts it offers has changed. +/// +/// +/// +/// This may be issued by servers without any previous subscription from the client. +/// +/// +/// See the schema for details. +/// +/// +public class PromptListChangedNotificationParams : NotificationParams; diff --git a/src/ModelContextProtocol.Core/Protocol/ReadResourceResult.cs b/src/ModelContextProtocol.Core/Protocol/ReadResourceResult.cs index e5ae44b1..fc60bf37 100644 --- a/src/ModelContextProtocol.Core/Protocol/ReadResourceResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ReadResourceResult.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class ReadResourceResult +public class ReadResourceResult : Result { /// /// Gets or sets a list of objects that this resource contains. diff --git a/src/ModelContextProtocol.Core/Protocol/RequestParams.cs b/src/ModelContextProtocol.Core/Protocol/RequestParams.cs index 20884b7b..6abf1d61 100644 --- a/src/ModelContextProtocol.Core/Protocol/RequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/RequestParams.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -11,12 +12,52 @@ namespace ModelContextProtocol.Protocol; public abstract class RequestParams { /// - /// Gets or sets metadata related to the request that provides additional protocol-level information. + /// Gets or sets metadata reserved by MCP for protocol-level metadata. /// /// - /// This can include progress tracking tokens and other protocol-specific properties - /// that are not part of the primary request parameters. + /// Implementations must not make assumptions about its contents. /// [JsonPropertyName("_meta")] - public RequestParamsMetadata? Meta { get; init; } + public JsonObject? Meta { get; set; } + + /// + /// Gets or sets an opaque token that will be attached to any subsequent progress notifications. + /// + [JsonIgnore] + public ProgressToken? ProgressToken + { + get + { + if (Meta?["progressToken"] is JsonValue progressToken) + { + if (progressToken.TryGetValue(out string? stringValue)) + { + return new ProgressToken(stringValue); + } + + if (progressToken.TryGetValue(out long longValue)) + { + return new ProgressToken(longValue); + } + } + + return null; + } + set + { + if (value is null) + { + Meta?.Remove("progressToken"); + } + else + { + (Meta ??= [])["progressToken"] = value.Value.Token switch + { + string s => JsonValue.Create(s), + long l => JsonValue.Create(l), + _ => throw new InvalidOperationException("ProgressToken must be a string or a long.") + }; + } + } + } } diff --git a/src/ModelContextProtocol.Core/Protocol/Resource.cs b/src/ModelContextProtocol.Core/Protocol/Resource.cs index a2dfd8e5..01cc5446 100644 --- a/src/ModelContextProtocol.Core/Protocol/Resource.cs +++ b/src/ModelContextProtocol.Core/Protocol/Resource.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -10,12 +11,6 @@ namespace ModelContextProtocol.Protocol; /// public class Resource : IBaseMetadata { - /// - /// Gets or sets the URI of this resource. - /// - [JsonPropertyName("uri")] - public required string Uri { get; init; } - /// [JsonPropertyName("name")] public required string Name { get; set; } @@ -24,6 +19,12 @@ public class Resource : IBaseMetadata [JsonPropertyName("title")] public string? Title { get; set; } + /// + /// Gets or sets the URI of this resource. + /// + [JsonPropertyName("uri")] + public required string Uri { get; init; } + /// /// Gets or sets a description of what this resource represents. /// @@ -59,6 +60,16 @@ public class Resource : IBaseMetadata [JsonPropertyName("mimeType")] public string? MimeType { get; init; } + /// + /// Gets or sets optional annotations for the resource. + /// + /// + /// These annotations can be used to specify the intended audience (, , or both) + /// and the priority level of the resource. Clients can use this information to filter or prioritize resources for different roles. + /// + [JsonPropertyName("annotations")] + public Annotations? Annotations { get; init; } + /// /// Gets or sets the size of the raw resource content (before base64 encoding), in bytes, if known. /// @@ -69,12 +80,11 @@ public class Resource : IBaseMetadata public long? Size { get; init; } /// - /// Gets or sets optional annotations for the resource. + /// Gets or sets metadata reserved by MCP for protocol-level metadata. /// /// - /// These annotations can be used to specify the intended audience (, , or both) - /// and the priority level of the resource. Clients can use this information to filter or prioritize resources for different roles. + /// Implementations must not make assumptions about its contents. /// - [JsonPropertyName("annotations")] - public Annotations? Annotations { get; init; } + [JsonPropertyName("_meta")] + public JsonObject? Meta { get; init; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs b/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs index 79917362..6e6754f1 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -44,6 +45,15 @@ private protected ResourceContents() [JsonPropertyName("mimeType")] public string? MimeType { 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 . /// @@ -67,6 +77,7 @@ public class Converter : JsonConverter string? mimeType = null; string? blob = null; string? text = null; + JsonObject? meta = null; while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { @@ -97,6 +108,10 @@ public class Converter : JsonConverter text = reader.GetString(); break; + case "_meta": + meta = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonObject); + break; + default: break; } @@ -108,7 +123,8 @@ public class Converter : JsonConverter { Uri = uri ?? string.Empty, MimeType = mimeType, - Blob = blob + Blob = blob, + Meta = meta, }; } @@ -118,7 +134,8 @@ public class Converter : JsonConverter { Uri = uri ?? string.Empty, MimeType = mimeType, - Text = text + Text = text, + Meta = meta, }; } @@ -148,6 +165,12 @@ public override void Write(Utf8JsonWriter writer, ResourceContents value, JsonSe writer.WriteString("text", textResource.Text); } + if (value.Meta is not null) + { + writer.WritePropertyName("_meta"); + JsonSerializer.Serialize(writer, value.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); + } + writer.WriteEndObject(); } } diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceListChangedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/ResourceListChangedNotificationParams.cs new file mode 100644 index 00000000..d481ee47 --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/ResourceListChangedNotificationParams.cs @@ -0,0 +1,15 @@ +namespace ModelContextProtocol.Protocol; + +/// +/// Represents the parameters used with a +/// notification from the server to the client, informing it that the list of resources it can read from has changed. +/// +/// +/// +/// This may be issued by servers without any previous subscription from the client. +/// +/// +/// See the schema for details. +/// +/// +public class ResourceListChangedNotificationParams : NotificationParams; diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs b/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs index 514eebb1..3f5d19ba 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -70,6 +71,15 @@ public class ResourceTemplate : IBaseMetadata [JsonPropertyName("annotations")] public Annotations? Annotations { get; init; } + /// + /// 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; init; } + /// Gets whether contains any template expressions. [JsonIgnore] public bool IsTemplated => UriTemplate.Contains('{'); @@ -91,6 +101,7 @@ public class ResourceTemplate : IBaseMetadata Description = Description, MimeType = MimeType, Annotations = Annotations, + Meta = Meta, }; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs index 52c12048..cf48dca0 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs @@ -17,7 +17,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ResourceUpdatedNotificationParams +public class ResourceUpdatedNotificationParams : NotificationParams { /// /// Gets or sets the URI of the resource that was updated. diff --git a/src/ModelContextProtocol.Core/Protocol/Result.cs b/src/ModelContextProtocol.Core/Protocol/Result.cs new file mode 100644 index 00000000..407bd96e --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/Result.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace ModelContextProtocol.Protocol; + +/// +/// Provides a base class for result payloads. +/// +public abstract class Result +{ + /// + /// 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; init; } +} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/Root.cs b/src/ModelContextProtocol.Core/Protocol/Root.cs index 72604bd4..bca6836c 100644 --- a/src/ModelContextProtocol.Core/Protocol/Root.cs +++ b/src/ModelContextProtocol.Core/Protocol/Root.cs @@ -32,6 +32,6 @@ public class Root /// /// This is reserved by the protocol for future use. /// - [JsonPropertyName("meta")] + [JsonPropertyName("_meta")] public JsonElement? Meta { get; init; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/RootsListChangedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/RootsListChangedNotificationParams.cs new file mode 100644 index 00000000..f156e0b5 --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/RootsListChangedNotificationParams.cs @@ -0,0 +1,15 @@ +namespace ModelContextProtocol.Protocol; + +/// +/// Represents the parameters used with a +/// notification from the client to the server, informing it that the list of roots has changed. +/// +/// +/// +/// This may be issued by servers without any previous subscription from the client. +/// +/// +/// See the schema for details. +/// +/// +public class RootsListChangedNotificationParams : NotificationParams; diff --git a/src/ModelContextProtocol.Core/Protocol/Tool.cs b/src/ModelContextProtocol.Core/Protocol/Tool.cs index 95636392..02b3c906 100644 --- a/src/ModelContextProtocol.Core/Protocol/Tool.cs +++ b/src/ModelContextProtocol.Core/Protocol/Tool.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -104,4 +105,13 @@ public JsonElement? OutputSchema /// [JsonPropertyName("annotations")] public ToolAnnotations? 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; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ToolListChangedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/ToolListChangedNotificationParams.cs new file mode 100644 index 00000000..0efb69c8 --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/ToolListChangedNotificationParams.cs @@ -0,0 +1,15 @@ +namespace ModelContextProtocol.Protocol; + +/// +/// Represents the parameters used with a +/// notification from the server to the client, informing it that the list of tools it offers has changed. +/// +/// +/// +/// This may be issued by servers without any previous subscription from the client. +/// +/// +/// See the schema for details. +/// +/// +public class ToolListChangedNotificationParams : NotificationParams; diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs index a1948308..b62ce2e9 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs @@ -103,7 +103,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( { var requestContent = GetRequestContext(args); if (requestContent?.Server is { } server && - requestContent?.Params?.Meta?.ProgressToken is { } progressToken) + requestContent?.Params?.ProgressToken is { } progressToken) { return new TokenProgress(server, progressToken); } diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs index 2edd5e48..86e34fc9 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs @@ -110,7 +110,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( { var requestContent = GetRequestContext(args); if (requestContent?.Server is { } server && - requestContent?.Params?.Meta?.ProgressToken is { } progressToken) + requestContent?.Params?.ProgressToken is { } progressToken) { return new TokenProgress(server, progressToken); } diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index 7cdcce3c..08c610f6 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -118,7 +118,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( { var requestContent = GetRequestContext(args); if (requestContent?.Server is { } server && - requestContent?.Params?.Meta?.ProgressToken is { } progressToken) + requestContent?.Params?.ProgressToken is { } progressToken) { return new TokenProgress(server, progressToken); } diff --git a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs index 4160fc3f..68874df3 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs @@ -34,7 +34,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . /// /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs index 921ac12b..c71e969d 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs @@ -25,7 +25,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . /// /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerResource.cs b/src/ModelContextProtocol.Core/Server/McpServerResource.cs index c7ae8fda..8e42d3e1 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResource.cs @@ -35,7 +35,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . /// /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs index c75d1b10..bc2f138f 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs @@ -23,7 +23,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . /// /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerTool.cs b/src/ModelContextProtocol.Core/Server/McpServerTool.cs index 95b9d0f4..e3958271 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerTool.cs @@ -35,7 +35,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . The parameter is not included in the generated JSON schema. /// /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs index 247516f0..97b0a38b 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs @@ -26,7 +26,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . The parameter is not included in the generated JSON schema. /// /// diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs index 771f6be7..8c7f736d 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs @@ -371,7 +371,7 @@ public async Task Progress_IsReported_InSameSseResponseAsRpcResponse() if (currentSseItem <= 10) { var notification = JsonSerializer.Deserialize(sseEvent, GetJsonTypeInfo()); - var progressNotification = AssertType(notification?.Params); + var progressNotification = AssertType(notification?.Params); Assert.Equal($"Progress {currentSseItem - 1}", progressNotification.Progress.Message); } else diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs index 96da34cf..e3d7ce44 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs @@ -49,10 +49,6 @@ public async Task CreateSamplingHandler_ShouldHandleTextMessages(float? temperat ], Temperature = temperature, MaxTokens = maxTokens, - Meta = new RequestParamsMetadata - { - ProgressToken = new ProgressToken(), - } }; var cancellationToken = CancellationToken.None; diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index 05492434..a8ee21d7 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -442,7 +442,7 @@ public async Task Notifications_Stdio(string clientId) await using var client = await _fixture.CreateClientAsync(clientId); // Verify we can send notifications without errors - await client.SendNotificationAsync(NotificationMethods.RootsUpdatedNotification, cancellationToken: TestContext.Current.CancellationToken); + await client.SendNotificationAsync(NotificationMethods.RootsListChangedNotification, cancellationToken: TestContext.Current.CancellationToken); await client.SendNotificationAsync("test/notification", new TestNotification { Test = true }, cancellationToken: TestContext.Current.CancellationToken, serializerOptions: JsonContext3.Default.Options); // assert diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index b98605d7..1fb7bcc7 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -612,10 +612,10 @@ public async Task HandlesIProgressParameter() TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); int remainingNotifications = 10; - ConcurrentQueue notifications = new(); + ConcurrentQueue notifications = new(); await using (client.RegisterNotificationHandler(NotificationMethods.ProgressNotification, (notification, cancellationToken) => { - if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions) is { } pn && + if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions) is { } pn && pn.ProgressToken == new ProgressToken("abc123")) { notifications.Enqueue(pn); @@ -633,7 +633,7 @@ public async Task HandlesIProgressParameter() new CallToolRequestParams { Name = progressTool.ProtocolTool.Name, - Meta = new() { ProgressToken = new("abc123") }, + ProgressToken = new("abc123"), }, cancellationToken: TestContext.Current.CancellationToken); @@ -641,7 +641,7 @@ public async Task HandlesIProgressParameter() Assert.Contains("done", JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions)); } - ProgressNotification[] array = notifications.OrderBy(n => n.Progress.Progress).ToArray(); + ProgressNotificationParams[] array = notifications.OrderBy(n => n.Progress.Progress).ToArray(); Assert.Equal(10, array.Length); for (int i = 0; i < array.Length; i++) { @@ -671,7 +671,7 @@ public async Task CancellationNotificationsPropagateToToolTokens() await client.SendNotificationAsync( NotificationMethods.CancelledNotification, - parameters: new CancelledNotification() + parameters: new CancelledNotificationParams() { RequestId = requestId, }, diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 03fae211..d72efa72 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -685,7 +685,7 @@ public async Task NotifyProgress_Should_Be_Handled() await transport.SendMessageAsync(new JsonRpcNotification { Method = NotificationMethods.ProgressNotification, - Params = JsonSerializer.SerializeToNode(new ProgressNotification + Params = JsonSerializer.SerializeToNode(new ProgressNotificationParams { ProgressToken = new("abc"), Progress = new() @@ -698,7 +698,7 @@ await transport.SendMessageAsync(new JsonRpcNotification }, TestContext.Current.CancellationToken); var notification = await notificationReceived.Task; - var progress = JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions); + var progress = JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions); Assert.NotNull(progress); Assert.Equal("abc", progress.ProgressToken.ToString()); Assert.Equal(50, progress.Progress.Progress); From a69fb3dd708a9b0333ae53f822a7d3bf64a6099d Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 16 Jun 2025 18:53:52 -0400 Subject: [PATCH 4/4] Address feedback, more cleanup, fix NAOT test failure --- .../Tools/AnnotatedMessageTool.cs | 6 +- .../EverythingServer/Tools/SampleLlmTool.cs | 2 +- samples/QuickstartWeatherServer/Program.cs | 2 +- .../Tools/SampleLlmTool.cs | 2 +- .../AIContentExtensions.cs | 4 +- .../Client/McpClientExtensions.cs | 2 +- .../Client/McpClientOptions.cs | 2 +- .../Client/SseClientTransportOptions.cs | 4 +- .../Client/StdioClientTransportOptions.cs | 4 +- .../StreamableHttpClientSessionTransport.cs | 2 +- .../McpJsonUtilities.cs | 13 ++- .../ProgressNotificationValue.cs | 2 +- .../Protocol/Annotations.cs | 10 +-- .../Protocol/Argument.cs | 2 +- .../Protocol/BlobResourceContents.cs | 2 +- .../Protocol/CallToolRequestParams.cs | 2 +- .../Protocol/CallToolResult.cs | 4 +- .../Protocol/ClientCapabilities.cs | 4 +- .../Protocol/CompleteContext.cs | 2 +- .../Protocol/CompleteRequestParams.cs | 2 +- .../Protocol/CompleteResult.cs | 2 +- .../Protocol/Completion.cs | 4 +- .../Protocol/CompletionsCapability.cs | 2 +- .../Protocol/ContentBlock.cs | 90 +++++++++---------- .../Protocol/CreateMessageRequestParams.cs | 2 +- .../Protocol/CreateMessageResult.cs | 2 +- .../Protocol/ElicitRequestParams.cs | 3 +- .../Protocol/ElicitResult.cs | 2 +- .../Protocol/ElicitationCapability.cs | 2 +- .../Protocol/EmptyResult.cs | 2 +- .../Protocol/GetPromptRequestParams.cs | 2 +- .../Protocol/GetPromptResult.cs | 4 +- .../Protocol/Implementation.cs | 2 +- .../Protocol/InitializeRequestParams.cs | 2 +- .../Protocol/InitializeResult.cs | 2 +- .../Protocol/InitializedNotificationParams.cs | 2 +- .../Protocol/JsonRpcError.cs | 2 +- .../Protocol/JsonRpcErrorDetail.cs | 2 +- .../Protocol/JsonRpcMessage.cs | 5 ++ .../Protocol/JsonRpcMessageWithId.cs | 5 ++ .../Protocol/JsonRpcNotification.cs | 2 +- .../Protocol/JsonRpcRequest.cs | 2 +- .../Protocol/JsonRpcResponse.cs | 2 +- .../Protocol/ListPromptsRequestParams.cs | 2 +- .../Protocol/ListPromptsResult.cs | 4 +- .../ListResourceTemplatesRequestParams.cs | 2 +- .../Protocol/ListResourceTemplatesResult.cs | 4 +- .../Protocol/ListResourcesRequestParams.cs | 2 +- .../Protocol/ListResourcesResult.cs | 4 +- .../Protocol/ListRootsRequestParams.cs | 2 +- .../Protocol/ListRootsResult.cs | 2 +- .../Protocol/ListToolsRequestParams.cs | 2 +- .../Protocol/ListToolsResult.cs | 4 +- .../Protocol/LoggingCapability.cs | 2 +- .../LoggingMessageNotificationParams.cs | 2 +- .../Protocol/ModelHint.cs | 2 +- .../Protocol/ModelPreferences.cs | 2 +- .../Protocol/NotificationParams.cs | 5 ++ .../Protocol/PaginatedRequest.cs | 5 ++ .../Protocol/PaginatedResult.cs | 4 + .../Protocol/PingResult.cs | 2 +- .../Protocol/ProgressNotificationParams.cs | 2 +- .../Protocol/Prompt.cs | 4 +- .../Protocol/PromptArgument.cs | 2 +- .../PromptListChangedNotification .cs | 2 +- .../Protocol/PromptMessage.cs | 4 +- .../Protocol/PromptsCapability.cs | 2 +- .../Protocol/ReadResourceRequestParams.cs | 2 +- .../Protocol/ReadResourceResult.cs | 4 +- .../Protocol/Reference.cs | 11 ++- .../Protocol/RequestParams.cs | 5 ++ .../Protocol/RequestParamsMetadata.cs | 2 +- .../Protocol/Resource.cs | 2 +- .../Protocol/ResourceContents.cs | 1 + .../ResourceListChangedNotificationParams.cs | 2 +- .../Protocol/ResourceTemplate.cs | 2 +- .../ResourceUpdatedNotificationParams.cs | 2 +- .../Protocol/ResourcesCapability.cs | 2 +- .../Protocol/Result.cs | 5 ++ .../Protocol/Root.cs | 2 +- .../Protocol/RootsCapability.cs | 2 +- .../RootsListChangedNotificationParams.cs | 2 +- .../Protocol/SamplingCapability.cs | 2 +- .../Protocol/SamplingMessage.cs | 2 +- .../Protocol/ServerCapabilities.cs | 4 +- .../Protocol/SetLevelRequestParams.cs | 2 +- .../Protocol/SubscribeRequestParams.cs | 2 +- .../Protocol/TextResourceContents.cs | 2 +- .../Protocol/Tool.cs | 2 +- .../Protocol/ToolAnnotations.cs | 2 +- .../ToolListChangedNotificationParams.cs | 2 +- .../Protocol/ToolsCapability.cs | 2 +- .../Protocol/UnsubscribeRequestParams.cs | 2 +- .../Server/AIFunctionMcpServerPrompt.cs | 2 +- .../Server/AIFunctionMcpServerResource.cs | 6 +- .../Server/AIFunctionMcpServerTool.cs | 8 +- .../Server/McpServer.cs | 26 +++++- .../Server/McpServerExtensions.cs | 2 +- .../Server/McpServerOptions.cs | 2 +- tests/Common/Utils/TestServerTransport.cs | 4 +- .../SseIntegrationTests.cs | 4 +- .../StreamableHttpClientConformanceTests.cs | 2 +- .../Program.cs | 22 ++--- .../Program.cs | 20 ++--- .../Client/McpClientResourceTemplateTests.cs | 6 +- .../McpServerBuilderExtensionsPromptsTests.cs | 6 +- ...cpServerBuilderExtensionsResourcesTests.cs | 2 +- .../McpServerBuilderExtensionsToolsTests.cs | 2 +- .../Configuration/McpServerScopedTests.cs | 2 +- .../Server/McpServerPromptTests.cs | 27 +++--- .../Server/McpServerResourceTests.cs | 16 ++-- .../Server/McpServerTests.cs | 2 +- .../Server/McpServerToolTests.cs | 16 ++-- .../Transport/SseClientTransportTests.cs | 2 +- 114 files changed, 303 insertions(+), 232 deletions(-) diff --git a/samples/EverythingServer/Tools/AnnotatedMessageTool.cs b/samples/EverythingServer/Tools/AnnotatedMessageTool.cs index e6ac535e..c14a2da2 100644 --- a/samples/EverythingServer/Tools/AnnotatedMessageTool.cs +++ b/samples/EverythingServer/Tools/AnnotatedMessageTool.cs @@ -19,17 +19,17 @@ public static IEnumerable AnnotatedMessage(MessageType messageType { List contents = messageType switch { - MessageType.Error => [new TextContentBlock() + MessageType.Error => [new TextContentBlock { Text = "Error: Operation failed", Annotations = new() { Audience = [Role.User, Role.Assistant], Priority = 1.0f } }], - MessageType.Success => [new TextContentBlock() + MessageType.Success => [new TextContentBlock { Text = "Operation completed successfully", Annotations = new() { Audience = [Role.User], Priority = 0.7f } }], - MessageType.Debug => [new TextContentBlock() + MessageType.Debug => [new TextContentBlock { Text = "Debug: Cache hit ratio 0.95, latency 150ms", Annotations = new() { Audience = [Role.Assistant], Priority = 0.3f } diff --git a/samples/EverythingServer/Tools/SampleLlmTool.cs b/samples/EverythingServer/Tools/SampleLlmTool.cs index d2ed9f2e..720cbead 100644 --- a/samples/EverythingServer/Tools/SampleLlmTool.cs +++ b/samples/EverythingServer/Tools/SampleLlmTool.cs @@ -27,7 +27,7 @@ private static CreateMessageRequestParams CreateRequestSamplingParams(string con Messages = [new SamplingMessage() { Role = Role.User, - Content = new TextContentBlock() { Text = $"Resource {uri} context: {context}" }, + Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/samples/QuickstartWeatherServer/Program.cs b/samples/QuickstartWeatherServer/Program.cs index 301eeed5..4e6216ee 100644 --- a/samples/QuickstartWeatherServer/Program.cs +++ b/samples/QuickstartWeatherServer/Program.cs @@ -17,7 +17,7 @@ builder.Services.AddSingleton(_ => { - var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") }; + var client = new HttpClient { BaseAddress = new Uri("https://api.weather.gov") }; client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0")); return client; }); diff --git a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs index 67cd33b3..aa03d7fb 100644 --- a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs +++ b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs @@ -30,7 +30,7 @@ private static CreateMessageRequestParams CreateRequestSamplingParams(string con Messages = [new SamplingMessage() { Role = Role.User, - Content = new TextContentBlock() { Text = $"Resource {uri} context: {context}" }, + Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/src/ModelContextProtocol.Core/AIContentExtensions.cs b/src/ModelContextProtocol.Core/AIContentExtensions.cs index 762e0706..e8c5d7e3 100644 --- a/src/ModelContextProtocol.Core/AIContentExtensions.cs +++ b/src/ModelContextProtocol.Core/AIContentExtensions.cs @@ -183,7 +183,7 @@ public static IList ToAIContents(this IEnumerable c internal static ContentBlock ToContent(this AIContent content) => content switch { - TextContent textContent => new TextContentBlock() + TextContent textContent => new TextContentBlock { Text = textContent.Text, }, @@ -209,7 +209,7 @@ internal static ContentBlock ToContent(this AIContent content) => } }, - _ => new TextContentBlock() + _ => new TextContentBlock { Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))), } diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index bc22275d..60a9c3a6 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -957,7 +957,7 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat return new() { - Content = content ?? new TextContentBlock() { Text = lastMessage?.Text ?? string.Empty }, + Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }, Model = chatResponse.ModelId ?? "unknown", Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", diff --git a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs index 96f36bbc..76099d0d 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Client; /// These options are typically passed to when creating a client. /// They define client capabilities, protocol version, and other client-specific settings. /// -public class McpClientOptions +public sealed class McpClientOptions { /// /// Gets or sets information about this client implementation, including its name and version. diff --git a/src/ModelContextProtocol.Core/Client/SseClientTransportOptions.cs b/src/ModelContextProtocol.Core/Client/SseClientTransportOptions.cs index b89a2b7f..cd522c42 100644 --- a/src/ModelContextProtocol.Core/Client/SseClientTransportOptions.cs +++ b/src/ModelContextProtocol.Core/Client/SseClientTransportOptions.cs @@ -3,7 +3,7 @@ namespace ModelContextProtocol.Client; /// /// Provides options for configuring instances. /// -public class SseClientTransportOptions +public sealed class SseClientTransportOptions { /// /// Gets or sets the base address of the server for SSE connections. @@ -69,5 +69,5 @@ public required Uri Endpoint /// /// Use this property to specify custom HTTP headers that should be sent with each request to the server. /// - public Dictionary? AdditionalHeaders { get; set; } + public IDictionary? AdditionalHeaders { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/StdioClientTransportOptions.cs b/src/ModelContextProtocol.Core/Client/StdioClientTransportOptions.cs index e4894860..65060224 100644 --- a/src/ModelContextProtocol.Core/Client/StdioClientTransportOptions.cs +++ b/src/ModelContextProtocol.Core/Client/StdioClientTransportOptions.cs @@ -3,7 +3,7 @@ namespace ModelContextProtocol.Client; /// /// Provides options for configuring instances. /// -public class StdioClientTransportOptions +public sealed class StdioClientTransportOptions { /// /// Gets or sets the command to execute to start the server process. @@ -53,7 +53,7 @@ public required string Command /// That includes removing the variables for any of this collection's entries with a null value. /// /// - public Dictionary? EnvironmentVariables { get; set; } + public IDictionary? EnvironmentVariables { get; set; } /// /// Gets or sets the timeout to wait for the server to shut down gracefully. diff --git a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs index 771f0cfd..981185dd 100644 --- a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs @@ -244,7 +244,7 @@ private void LogJsonException(JsonException ex, string data) } } - internal static void CopyAdditionalHeaders(HttpRequestHeaders headers, Dictionary? additionalHeaders, string? sessionId = null) + internal static void CopyAdditionalHeaders(HttpRequestHeaders headers, IDictionary? additionalHeaders, string? sessionId = null) { if (sessionId is not null) { diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs index 7d0cc8b0..696e0ec0 100644 --- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs +++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs @@ -138,10 +138,21 @@ internal static bool IsValidMcpToolSchema(JsonElement element) [JsonSerializable(typeof(SubscribeRequestParams))] [JsonSerializable(typeof(UnsubscribeRequestParams))] + // MCP Content + [JsonSerializable(typeof(ContentBlock))] + [JsonSerializable(typeof(TextContentBlock))] + [JsonSerializable(typeof(ImageContentBlock))] + [JsonSerializable(typeof(AudioContentBlock))] + [JsonSerializable(typeof(EmbeddedResourceBlock))] + [JsonSerializable(typeof(ResourceLinkBlock))] + [JsonSerializable(typeof(PromptReference))] + [JsonSerializable(typeof(ResourceTemplateReference))] + [JsonSerializable(typeof(BlobResourceContents))] + [JsonSerializable(typeof(TextResourceContents))] + // Other MCP Types [JsonSerializable(typeof(IReadOnlyDictionary))] [JsonSerializable(typeof(ProgressToken))] - [JsonSerializable(typeof(PromptMessage[]))] // Primitive types for use in consuming AIFunctions [JsonSerializable(typeof(string))] diff --git a/src/ModelContextProtocol.Core/ProgressNotificationValue.cs b/src/ModelContextProtocol.Core/ProgressNotificationValue.cs index f8ee0a5e..68ac73b6 100644 --- a/src/ModelContextProtocol.Core/ProgressNotificationValue.cs +++ b/src/ModelContextProtocol.Core/ProgressNotificationValue.cs @@ -1,7 +1,7 @@ namespace ModelContextProtocol; /// Provides a progress value that can be sent using . -public class ProgressNotificationValue +public sealed class ProgressNotificationValue { /// /// Gets or sets the progress thus far. diff --git a/src/ModelContextProtocol.Core/Protocol/Annotations.cs b/src/ModelContextProtocol.Core/Protocol/Annotations.cs index f9aa58a5..6f90182d 100644 --- a/src/ModelContextProtocol.Core/Protocol/Annotations.cs +++ b/src/ModelContextProtocol.Core/Protocol/Annotations.cs @@ -9,13 +9,13 @@ namespace ModelContextProtocol.Protocol; /// Annotations enable filtering and prioritization of content for different audiences. /// See the schema for details. /// -public class Annotations +public sealed class Annotations { /// /// Gets or sets the intended audience for this content as an array of values. /// [JsonPropertyName("audience")] - public Role[]? Audience { get; init; } + public IList? Audience { get; init; } /// /// Gets or sets a value indicating how important this data is for operating the server. @@ -28,12 +28,12 @@ public class Annotations public float? Priority { get; init; } /// - /// Gets or sets the moment the resource was last modified, as an ISO 8601 formatted string. + /// Gets or sets the moment the resource was last modified. /// /// - /// Should be an ISO 8601 formatted string (e.g., \"2025-01-12T15:00:58Z\"). + /// The corresponding JSON should be an ISO 8601 formatted string (e.g., \"2025-01-12T15:00:58Z\"). /// Examples: last activity timestamp in an open file, timestamp when the resource was attached, etc. /// [JsonPropertyName("lastModified")] - public string? LastModified { get; set; } + public DateTimeOffset? LastModified { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/Argument.cs b/src/ModelContextProtocol.Core/Protocol/Argument.cs index ad003f44..f4969294 100644 --- a/src/ModelContextProtocol.Core/Protocol/Argument.cs +++ b/src/ModelContextProtocol.Core/Protocol/Argument.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol; /// This class is used when requesting completion suggestions for a particular field or parameter. /// See the schema for details. /// -public class Argument +public sealed class Argument { /// /// Gets or sets the name of the argument being completed. diff --git a/src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs b/src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs index a15e0bda..017b13b3 100644 --- a/src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs +++ b/src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs @@ -20,7 +20,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for more details. /// /// -public class BlobResourceContents : ResourceContents +public sealed class BlobResourceContents : ResourceContents { /// /// Gets or sets the base64-encoded string representing the binary data of the item. diff --git a/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs index fbe4d3ff..832cf5bf 100644 --- a/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/CallToolRequestParams.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Protocol; /// The server will respond with a containing the result of the tool invocation. /// See the schema for details. /// -public class CallToolRequestParams : RequestParams +public sealed class CallToolRequestParams : RequestParams { /// Gets or sets the name of the tool to invoke. [JsonPropertyName("name")] diff --git a/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs b/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs index 43eb353f..7438522c 100644 --- a/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/CallToolResult.cs @@ -20,13 +20,13 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class CallToolResult : Result +public sealed class CallToolResult : Result { /// /// Gets or sets the response content from the tool call. /// [JsonPropertyName("content")] - public List Content { get; set; } = []; + public IList Content { get; set; } = []; /// /// Gets or sets an optional JSON object representing the structured result of the tool call. diff --git a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs index 88680e2a..ebe69813 100644 --- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs @@ -15,7 +15,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ClientCapabilities +public sealed class ClientCapabilities { /// /// Gets or sets experimental, non-standard capabilities that the client supports. @@ -33,7 +33,7 @@ public class ClientCapabilities /// /// [JsonPropertyName("experimental")] - public Dictionary? Experimental { get; set; } + public IDictionary? Experimental { get; set; } /// /// Gets or sets the client's roots capability, which are entry points for resource navigation. diff --git a/src/ModelContextProtocol.Core/Protocol/CompleteContext.cs b/src/ModelContextProtocol.Core/Protocol/CompleteContext.cs index eda5a50c..f5ea3787 100644 --- a/src/ModelContextProtocol.Core/Protocol/CompleteContext.cs +++ b/src/ModelContextProtocol.Core/Protocol/CompleteContext.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol; /// This context provides information that helps the server generate more relevant /// completion suggestions, such as previously resolved variables in a template. /// -public class CompleteContext +public sealed class CompleteContext { /// /// Gets or sets previously-resolved variables in a URI template or prompt. diff --git a/src/ModelContextProtocol.Core/Protocol/CompleteRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/CompleteRequestParams.cs index 1514ad3d..2798c7e6 100644 --- a/src/ModelContextProtocol.Core/Protocol/CompleteRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/CompleteRequestParams.cs @@ -17,7 +17,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class CompleteRequestParams : RequestParams +public sealed class CompleteRequestParams : RequestParams { /// /// Gets or sets the reference's information. diff --git a/src/ModelContextProtocol.Core/Protocol/CompleteResult.cs b/src/ModelContextProtocol.Core/Protocol/CompleteResult.cs index 4ca366bc..f1b9dd2f 100644 --- a/src/ModelContextProtocol.Core/Protocol/CompleteResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/CompleteResult.cs @@ -23,7 +23,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class CompleteResult : Result +public sealed class CompleteResult : Result { /// /// Gets or sets the completion object containing the suggested values and pagination information. diff --git a/src/ModelContextProtocol.Core/Protocol/Completion.cs b/src/ModelContextProtocol.Core/Protocol/Completion.cs index 029dac3e..cb562b71 100644 --- a/src/ModelContextProtocol.Core/Protocol/Completion.cs +++ b/src/ModelContextProtocol.Core/Protocol/Completion.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class Completion +public sealed class Completion { /// /// Gets or sets an array of completion values (auto-suggestions) for the requested input. @@ -19,7 +19,7 @@ public class Completion /// Per the specification, this should not exceed 100 items. /// [JsonPropertyName("values")] - public string[] Values { get; set; } = []; + public IList Values { get; set; } = []; /// /// Gets or sets the total number of completion options available. diff --git a/src/ModelContextProtocol.Core/Protocol/CompletionsCapability.cs b/src/ModelContextProtocol.Core/Protocol/CompletionsCapability.cs index 4ba8a4c8..f411c297 100644 --- a/src/ModelContextProtocol.Core/Protocol/CompletionsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/CompletionsCapability.cs @@ -20,7 +20,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class CompletionsCapability +public sealed class CompletionsCapability { // Currently empty in the spec, but may be extended in the future. diff --git a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs index 4440efa7..db86c7b6 100644 --- a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs +++ b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs @@ -24,7 +24,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for more details. /// /// -[JsonConverter(typeof(Converter))] +[JsonConverter(typeof(Converter))] // TODO: This converter exists due to the lack of downlevel support for AllowOutOfOrderMetadataProperties. public abstract class ContentBlock { /// Prevent external derivations. @@ -140,56 +140,50 @@ public class Converter : JsonConverter } } - switch (type) + return type switch { - case "text": - return new TextContentBlock() - { - Text = text ?? throw new JsonException("Text contents must be provided for 'text' type."), - Annotations = annotations, - Meta = meta, - }; + "text" => new TextContentBlock + { + Text = text ?? throw new JsonException("Text contents must be provided for 'text' type."), + Annotations = annotations, + Meta = meta, + }, - case "image": - return 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."), - Annotations = annotations, - Meta = meta, - }; - - case "audio": - return 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."), - Annotations = annotations, - Meta = meta, - }; - - case "resource": - return new EmbeddedResourceBlock() - { - Resource = resource ?? throw new JsonException("Resource contents must be provided for 'resource' type."), - Annotations = annotations, - Meta = meta, - }; + "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."), + Annotations = annotations, + Meta = meta, + }, - case "resource_link": - return 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."), - Description = description, - MimeType = mimeType, - Size = size, - Annotations = annotations, - }; - - default: - throw new JsonException($"Unknown content type: '{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."), + Annotations = annotations, + Meta = meta, + }, + + "resource" => new EmbeddedResourceBlock() + { + Resource = resource ?? throw new JsonException("Resource contents must be provided for 'resource' type."), + Annotations = annotations, + Meta = meta, + }, + + "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."), + Description = description, + MimeType = mimeType, + Size = size, + Annotations = annotations, + }, + + _ => throw new JsonException($"Unknown content type: '{type}'"), + }; } /// diff --git a/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs index a2418e14..b71358df 100644 --- a/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class CreateMessageRequestParams : RequestParams +public sealed class CreateMessageRequestParams : RequestParams { /// /// Gets or sets an indication as to which server contexts should be included in the prompt. diff --git a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs index d47c4e18..ba599d6c 100644 --- a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class CreateMessageResult : Result +public sealed class CreateMessageResult : Result { /// /// Gets or sets the content of the message. diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs index 2cee4d1f..05d8a49a 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol; /// /// Represents a message issued from the server to elicit additional information from the user via the client. /// -public class ElicitRequestParams +public sealed class ElicitRequestParams { /// /// Gets or sets the message to present to the user. @@ -65,6 +65,7 @@ public IDictionary Properties [JsonDerivedType(typeof(StringSchema))] public abstract class PrimitiveSchemaDefinition { + /// Prevent external derivations. protected private PrimitiveSchemaDefinition() { } diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs index 3b34cad2..39387f50 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol; /// /// Represents the client's response to an elicitation request. /// -public class ElicitResult : Result +public sealed class ElicitResult : Result { /// /// Gets or sets the user action in response to the elicitation. diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitationCapability.cs b/src/ModelContextProtocol.Core/Protocol/ElicitationCapability.cs index 7b918aff..d88247d2 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitationCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitationCapability.cs @@ -14,7 +14,7 @@ namespace ModelContextProtocol.Protocol; /// during interactions. The client must set a to process these requests. /// /// -public class ElicitationCapability +public sealed class ElicitationCapability { // Currently empty in the spec, but may be extended in the future. diff --git a/src/ModelContextProtocol.Core/Protocol/EmptyResult.cs b/src/ModelContextProtocol.Core/Protocol/EmptyResult.cs index fb86a2c2..cf26cc3d 100644 --- a/src/ModelContextProtocol.Core/Protocol/EmptyResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/EmptyResult.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol; /// Represents an empty result object for operations that need to indicate successful completion /// but don't need to return any specific data. /// -public class EmptyResult : Result +public sealed class EmptyResult : Result { [JsonIgnore] internal static EmptyResult Instance { get; } = new(); diff --git a/src/ModelContextProtocol.Core/Protocol/GetPromptRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/GetPromptRequestParams.cs index 52d82773..c94174a3 100644 --- a/src/ModelContextProtocol.Core/Protocol/GetPromptRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/GetPromptRequestParams.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Protocol; /// The server will respond with a containing the resulting prompt. /// See the schema for details. /// -public class GetPromptRequestParams : RequestParams +public sealed class GetPromptRequestParams : RequestParams { /// /// Gets or sets the name of the prompt. diff --git a/src/ModelContextProtocol.Core/Protocol/GetPromptResult.cs b/src/ModelContextProtocol.Core/Protocol/GetPromptResult.cs index bd79fd54..f78c5a32 100644 --- a/src/ModelContextProtocol.Core/Protocol/GetPromptResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/GetPromptResult.cs @@ -15,7 +15,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class GetPromptResult : Result +public sealed class GetPromptResult : Result { /// /// Gets or sets an optional description for the prompt. @@ -38,5 +38,5 @@ public class GetPromptResult : Result /// Gets or sets the prompt that the server offers. /// [JsonPropertyName("messages")] - public List Messages { get; set; } = []; + public IList Messages { get; set; } = []; } diff --git a/src/ModelContextProtocol.Core/Protocol/Implementation.cs b/src/ModelContextProtocol.Core/Protocol/Implementation.cs index b13e43f2..af177000 100644 --- a/src/ModelContextProtocol.Core/Protocol/Implementation.cs +++ b/src/ModelContextProtocol.Core/Protocol/Implementation.cs @@ -17,7 +17,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class Implementation : IBaseMetadata +public sealed class Implementation : IBaseMetadata { /// [JsonPropertyName("name")] diff --git a/src/ModelContextProtocol.Core/Protocol/InitializeRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/InitializeRequestParams.cs index 5af4d078..7aedd405 100644 --- a/src/ModelContextProtocol.Core/Protocol/InitializeRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/InitializeRequestParams.cs @@ -19,7 +19,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class InitializeRequestParams : RequestParams +public sealed class InitializeRequestParams : RequestParams { /// /// Gets or sets the version of the Model Context Protocol that the client wants to use. diff --git a/src/ModelContextProtocol.Core/Protocol/InitializeResult.cs b/src/ModelContextProtocol.Core/Protocol/InitializeResult.cs index 24d00ed2..831cc670 100644 --- a/src/ModelContextProtocol.Core/Protocol/InitializeResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/InitializeResult.cs @@ -19,7 +19,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class InitializeResult : Result +public sealed class InitializeResult : Result { /// /// Gets or sets the version of the Model Context Protocol that the server will use for this session. diff --git a/src/ModelContextProtocol.Core/Protocol/InitializedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/InitializedNotificationParams.cs index 7375e2d4..f77f1a2e 100644 --- a/src/ModelContextProtocol.Core/Protocol/InitializedNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/InitializedNotificationParams.cs @@ -7,4 +7,4 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class InitializedNotificationParams : NotificationParams; +public sealed class InitializedNotificationParams : NotificationParams; diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcError.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcError.cs index 2229634d..5de344db 100644 --- a/src/ModelContextProtocol.Core/Protocol/JsonRpcError.cs +++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcError.cs @@ -16,7 +16,7 @@ namespace ModelContextProtocol.Protocol; /// and optional additional data to provide more context about the error. /// /// -public class JsonRpcError : JsonRpcMessageWithId +public sealed class JsonRpcError : JsonRpcMessageWithId { /// /// Gets detailed error information for the failed request, containing an error code, diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcErrorDetail.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcErrorDetail.cs index bcdd4280..e1009ce9 100644 --- a/src/ModelContextProtocol.Core/Protocol/JsonRpcErrorDetail.cs +++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcErrorDetail.cs @@ -11,7 +11,7 @@ namespace ModelContextProtocol.Protocol; /// a standard format for error responses that includes a numeric code, a human-readable message, /// and optional additional data. /// -public class JsonRpcErrorDetail +public sealed class JsonRpcErrorDetail { /// /// Gets an integer error code according to the JSON-RPC specification. diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs index fde41507..77866add 100644 --- a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs +++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs @@ -15,6 +15,11 @@ namespace ModelContextProtocol.Protocol; [JsonConverter(typeof(Converter))] public abstract class JsonRpcMessage { + /// Prevent external derivations. + private protected JsonRpcMessage() + { + } + /// /// Gets the JSON-RPC protocol version used. /// diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs index b0826193..8233df48 100644 --- a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs +++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs @@ -14,6 +14,11 @@ namespace ModelContextProtocol.Protocol; /// public abstract class JsonRpcMessageWithId : JsonRpcMessage { + /// Prevent external derivations. + private protected JsonRpcMessageWithId() + { + } + /// /// Gets the message identifier. /// diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcNotification.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcNotification.cs index 93d8f408..6a206515 100644 --- a/src/ModelContextProtocol.Core/Protocol/JsonRpcNotification.cs +++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcNotification.cs @@ -11,7 +11,7 @@ namespace ModelContextProtocol.Protocol; /// They are useful for one-way communication, such as log notifications and progress updates. /// Unlike requests, notifications do not include an ID field, since there will be no response to match with it. /// -public class JsonRpcNotification : JsonRpcMessage +public sealed class JsonRpcNotification : JsonRpcMessage { /// /// Gets or sets the name of the notification method. diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcRequest.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcRequest.cs index 70072dee..ed6c8982 100644 --- a/src/ModelContextProtocol.Core/Protocol/JsonRpcRequest.cs +++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcRequest.cs @@ -14,7 +14,7 @@ namespace ModelContextProtocol.Protocol; /// and return either a with the result, or a /// if the method execution fails. /// -public class JsonRpcRequest : JsonRpcMessageWithId +public sealed class JsonRpcRequest : JsonRpcMessageWithId { /// /// Name of the method to invoke. diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcResponse.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcResponse.cs index 098db137..c7d824b7 100644 --- a/src/ModelContextProtocol.Core/Protocol/JsonRpcResponse.cs +++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcResponse.cs @@ -16,7 +16,7 @@ namespace ModelContextProtocol.Protocol; /// This class represents a successful response with a result. For error responses, see . /// /// -public class JsonRpcResponse : JsonRpcMessageWithId +public sealed class JsonRpcResponse : JsonRpcMessageWithId { /// /// Gets the result of the method invocation. diff --git a/src/ModelContextProtocol.Core/Protocol/ListPromptsRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ListPromptsRequestParams.cs index 7202ef2d..c82ee243 100644 --- a/src/ModelContextProtocol.Core/Protocol/ListPromptsRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ListPromptsRequestParams.cs @@ -8,4 +8,4 @@ namespace ModelContextProtocol.Protocol; /// The server responds with a containing the available prompts. /// See the schema for details. /// -public class ListPromptsRequestParams : PaginatedRequestParams; +public sealed class ListPromptsRequestParams : PaginatedRequestParams; diff --git a/src/ModelContextProtocol.Core/Protocol/ListPromptsResult.cs b/src/ModelContextProtocol.Core/Protocol/ListPromptsResult.cs index 65b9e7c9..09262a2a 100644 --- a/src/ModelContextProtocol.Core/Protocol/ListPromptsResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ListPromptsResult.cs @@ -18,11 +18,11 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ListPromptsResult : PaginatedResult +public sealed class ListPromptsResult : PaginatedResult { /// /// A list of prompts or prompt templates that the server offers. /// [JsonPropertyName("prompts")] - public List Prompts { get; set; } = []; + public IList Prompts { get; set; } = []; } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/ListResourceTemplatesRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ListResourceTemplatesRequestParams.cs index 0418b8ce..0c431f3f 100644 --- a/src/ModelContextProtocol.Core/Protocol/ListResourceTemplatesRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ListResourceTemplatesRequestParams.cs @@ -8,4 +8,4 @@ namespace ModelContextProtocol.Protocol; /// The server responds with a containing the available resource templates. /// See the schema for details. /// -public class ListResourceTemplatesRequestParams : PaginatedRequestParams; \ No newline at end of file +public sealed class ListResourceTemplatesRequestParams : PaginatedRequestParams; \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/ListResourceTemplatesResult.cs b/src/ModelContextProtocol.Core/Protocol/ListResourceTemplatesResult.cs index 9b2bce03..6e422a75 100644 --- a/src/ModelContextProtocol.Core/Protocol/ListResourceTemplatesResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ListResourceTemplatesResult.cs @@ -20,7 +20,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ListResourceTemplatesResult : PaginatedResult +public sealed class ListResourceTemplatesResult : PaginatedResult { /// /// Gets or sets a list of resource templates that the server offers. @@ -31,5 +31,5 @@ public class ListResourceTemplatesResult : PaginatedResult /// including URI templates, names, descriptions, and MIME types. /// [JsonPropertyName("resourceTemplates")] - public List ResourceTemplates { get; set; } = []; + public IList ResourceTemplates { get; set; } = []; } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/ListResourcesRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ListResourcesRequestParams.cs index 05e7fc26..60cb7591 100644 --- a/src/ModelContextProtocol.Core/Protocol/ListResourcesRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ListResourcesRequestParams.cs @@ -8,4 +8,4 @@ namespace ModelContextProtocol.Protocol; /// The server responds with a containing the available resources. /// See the schema for details. /// -public class ListResourcesRequestParams : PaginatedRequestParams; +public sealed class ListResourcesRequestParams : PaginatedRequestParams; diff --git a/src/ModelContextProtocol.Core/Protocol/ListResourcesResult.cs b/src/ModelContextProtocol.Core/Protocol/ListResourcesResult.cs index 9e50b54e..fde65d5b 100644 --- a/src/ModelContextProtocol.Core/Protocol/ListResourcesResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ListResourcesResult.cs @@ -18,11 +18,11 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ListResourcesResult : PaginatedResult +public sealed class ListResourcesResult : PaginatedResult { /// /// A list of resources that the server offers. /// [JsonPropertyName("resources")] - public List Resources { get; set; } = []; + public IList Resources { get; set; } = []; } diff --git a/src/ModelContextProtocol.Core/Protocol/ListRootsRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ListRootsRequestParams.cs index b0911daa..c18e2666 100644 --- a/src/ModelContextProtocol.Core/Protocol/ListRootsRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ListRootsRequestParams.cs @@ -8,4 +8,4 @@ namespace ModelContextProtocol.Protocol; /// The client responds with a containing the client's roots. /// See the schema for details. /// -public class ListRootsRequestParams : RequestParams; +public sealed class ListRootsRequestParams : RequestParams; diff --git a/src/ModelContextProtocol.Core/Protocol/ListRootsResult.cs b/src/ModelContextProtocol.Core/Protocol/ListRootsResult.cs index 27ce1ad9..38424917 100644 --- a/src/ModelContextProtocol.Core/Protocol/ListRootsResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ListRootsResult.cs @@ -16,7 +16,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ListRootsResult : Result +public sealed class ListRootsResult : Result { /// /// Gets or sets the list of root URIs provided by the client. diff --git a/src/ModelContextProtocol.Core/Protocol/ListToolsRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ListToolsRequestParams.cs index 0a32729a..205d70be 100644 --- a/src/ModelContextProtocol.Core/Protocol/ListToolsRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ListToolsRequestParams.cs @@ -8,4 +8,4 @@ namespace ModelContextProtocol.Protocol; /// The server responds with a containing the available tools. /// See the schema for details. /// -public class ListToolsRequestParams : PaginatedRequestParams; +public sealed class ListToolsRequestParams : PaginatedRequestParams; diff --git a/src/ModelContextProtocol.Core/Protocol/ListToolsResult.cs b/src/ModelContextProtocol.Core/Protocol/ListToolsResult.cs index feef775e..51ee3e83 100644 --- a/src/ModelContextProtocol.Core/Protocol/ListToolsResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ListToolsResult.cs @@ -18,11 +18,11 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ListToolsResult : PaginatedResult +public sealed class ListToolsResult : PaginatedResult { /// /// The server's response to a tools/list request from the client. /// [JsonPropertyName("tools")] - public List Tools { get; set; } = []; + public IList Tools { get; set; } = []; } diff --git a/src/ModelContextProtocol.Core/Protocol/LoggingCapability.cs b/src/ModelContextProtocol.Core/Protocol/LoggingCapability.cs index 9cf62b66..ab43fb06 100644 --- a/src/ModelContextProtocol.Core/Protocol/LoggingCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/LoggingCapability.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Protocol; /// This capability allows clients to set the logging level and receive log messages from the server. /// See the schema for details. /// -public class LoggingCapability +public sealed class LoggingCapability { // Currently empty in the spec, but may be extended in the future diff --git a/src/ModelContextProtocol.Core/Protocol/LoggingMessageNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/LoggingMessageNotificationParams.cs index bf2236b5..8c9167dd 100644 --- a/src/ModelContextProtocol.Core/Protocol/LoggingMessageNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/LoggingMessageNotificationParams.cs @@ -20,7 +20,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class LoggingMessageNotificationParams : NotificationParams +public sealed class LoggingMessageNotificationParams : NotificationParams { /// /// Gets or sets the severity of this log message. diff --git a/src/ModelContextProtocol.Core/Protocol/ModelHint.cs b/src/ModelContextProtocol.Core/Protocol/ModelHint.cs index 4ec929ef..30e18cb5 100644 --- a/src/ModelContextProtocol.Core/Protocol/ModelHint.cs +++ b/src/ModelContextProtocol.Core/Protocol/ModelHint.cs @@ -14,7 +14,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ModelHint +public sealed class ModelHint { /// /// Gets or sets a hint for a model name. diff --git a/src/ModelContextProtocol.Core/Protocol/ModelPreferences.cs b/src/ModelContextProtocol.Core/Protocol/ModelPreferences.cs index a91b3da6..8d9d13fc 100644 --- a/src/ModelContextProtocol.Core/Protocol/ModelPreferences.cs +++ b/src/ModelContextProtocol.Core/Protocol/ModelPreferences.cs @@ -22,7 +22,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ModelPreferences +public sealed class ModelPreferences { /// /// Gets or sets how much to prioritize cost when selecting a model. diff --git a/src/ModelContextProtocol.Core/Protocol/NotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/NotificationParams.cs index 29668863..54432a4c 100644 --- a/src/ModelContextProtocol.Core/Protocol/NotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/NotificationParams.cs @@ -8,6 +8,11 @@ namespace ModelContextProtocol.Protocol; /// public abstract class NotificationParams { + /// Prevent external derivations. + private protected NotificationParams() + { + } + /// /// Gets or sets metadata reserved by MCP for protocol-level metadata. /// diff --git a/src/ModelContextProtocol.Core/Protocol/PaginatedRequest.cs b/src/ModelContextProtocol.Core/Protocol/PaginatedRequest.cs index 1ab08d27..0dc43c66 100644 --- a/src/ModelContextProtocol.Core/Protocol/PaginatedRequest.cs +++ b/src/ModelContextProtocol.Core/Protocol/PaginatedRequest.cs @@ -10,6 +10,11 @@ namespace ModelContextProtocol.Protocol; /// public abstract class PaginatedRequestParams : RequestParams { + /// Prevent external derivations. + private protected PaginatedRequestParams() + { + } + /// /// Gets or sets an opaque token representing the current pagination position. /// diff --git a/src/ModelContextProtocol.Core/Protocol/PaginatedResult.cs b/src/ModelContextProtocol.Core/Protocol/PaginatedResult.cs index 1512dc37..0f81455f 100644 --- a/src/ModelContextProtocol.Core/Protocol/PaginatedResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/PaginatedResult.cs @@ -17,6 +17,10 @@ namespace ModelContextProtocol.Protocol; /// public abstract class PaginatedResult : Result { + private protected PaginatedResult() + { + } + /// /// Gets or sets an opaque token representing the pagination position after the last returned result. /// diff --git a/src/ModelContextProtocol.Core/Protocol/PingResult.cs b/src/ModelContextProtocol.Core/Protocol/PingResult.cs index 8dfef3f5..08e5094e 100644 --- a/src/ModelContextProtocol.Core/Protocol/PingResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/PingResult.cs @@ -14,4 +14,4 @@ namespace ModelContextProtocol.Protocol; /// is still responsive. /// /// -public class PingResult : Result; \ No newline at end of file +public sealed class PingResult : Result; \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs index 059aed57..1fd92706 100644 --- a/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ProgressNotificationParams.cs @@ -13,7 +13,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for more details. /// [JsonConverter(typeof(Converter))] -public class ProgressNotificationParams : NotificationParams +public sealed class ProgressNotificationParams : NotificationParams { /// /// Gets or sets the progress token which was given in the initial request, used to associate this notification with diff --git a/src/ModelContextProtocol.Core/Protocol/Prompt.cs b/src/ModelContextProtocol.Core/Protocol/Prompt.cs index 580ac705..1a500406 100644 --- a/src/ModelContextProtocol.Core/Protocol/Prompt.cs +++ b/src/ModelContextProtocol.Core/Protocol/Prompt.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class Prompt : IBaseMetadata +public sealed class Prompt : IBaseMetadata { /// [JsonPropertyName("name")] @@ -49,7 +49,7 @@ public class Prompt : IBaseMetadata /// /// [JsonPropertyName("arguments")] - public List? Arguments { get; set; } + public IList? Arguments { get; set; } /// /// Gets or sets metadata reserved by MCP for protocol-level metadata. diff --git a/src/ModelContextProtocol.Core/Protocol/PromptArgument.cs b/src/ModelContextProtocol.Core/Protocol/PromptArgument.cs index 944f4da3..648eccc0 100644 --- a/src/ModelContextProtocol.Core/Protocol/PromptArgument.cs +++ b/src/ModelContextProtocol.Core/Protocol/PromptArgument.cs @@ -15,7 +15,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class PromptArgument : IBaseMetadata +public sealed class PromptArgument : IBaseMetadata { /// [JsonPropertyName("name")] diff --git a/src/ModelContextProtocol.Core/Protocol/PromptListChangedNotification .cs b/src/ModelContextProtocol.Core/Protocol/PromptListChangedNotification .cs index 7b3ff7f5..8d8a5cc6 100644 --- a/src/ModelContextProtocol.Core/Protocol/PromptListChangedNotification .cs +++ b/src/ModelContextProtocol.Core/Protocol/PromptListChangedNotification .cs @@ -12,4 +12,4 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class PromptListChangedNotificationParams : NotificationParams; +public sealed class PromptListChangedNotificationParams : NotificationParams; diff --git a/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs b/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs index 65418a67..4bf8b28e 100644 --- a/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs +++ b/src/ModelContextProtocol.Core/Protocol/PromptMessage.cs @@ -26,7 +26,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class PromptMessage +public sealed class PromptMessage { /// /// Gets or sets the content of the message, which can be text, image, audio, or an embedded resource. @@ -37,7 +37,7 @@ public class PromptMessage /// The property indicates the specific content type. /// [JsonPropertyName("content")] - public ContentBlock Content { get; set; } = new TextContentBlock() { Text = "" }; + public ContentBlock Content { get; set; } = new TextContentBlock { Text = "" }; /// /// Gets or sets the role of the message sender, specifying whether it's from a "user" or an "assistant". diff --git a/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs b/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs index 79fe4662..8fad1c0e 100644 --- a/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs @@ -16,7 +16,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class PromptsCapability +public sealed class PromptsCapability { /// /// Gets or sets whether this server supports notifications for changes to the prompt list. diff --git a/src/ModelContextProtocol.Core/Protocol/ReadResourceRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ReadResourceRequestParams.cs index 8b1f1948..680909c0 100644 --- a/src/ModelContextProtocol.Core/Protocol/ReadResourceRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ReadResourceRequestParams.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol; /// The server will respond with a containing the resulting resource data. /// See the schema for details. /// -public class ReadResourceRequestParams : RequestParams +public sealed class ReadResourceRequestParams : RequestParams { /// /// The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it. diff --git a/src/ModelContextProtocol.Core/Protocol/ReadResourceResult.cs b/src/ModelContextProtocol.Core/Protocol/ReadResourceResult.cs index fc60bf37..084322fd 100644 --- a/src/ModelContextProtocol.Core/Protocol/ReadResourceResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ReadResourceResult.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class ReadResourceResult : Result +public sealed class ReadResourceResult : Result { /// /// Gets or sets a list of objects that this resource contains. @@ -19,5 +19,5 @@ public class ReadResourceResult : Result /// The type of content included depends on the resource being accessed. /// [JsonPropertyName("contents")] - public List Contents { get; set; } = []; + public IList Contents { get; set; } = []; } diff --git a/src/ModelContextProtocol.Core/Protocol/Reference.cs b/src/ModelContextProtocol.Core/Protocol/Reference.cs index 397bcb5b..a9c87fe4 100644 --- a/src/ModelContextProtocol.Core/Protocol/Reference.cs +++ b/src/ModelContextProtocol.Core/Protocol/Reference.cs @@ -22,7 +22,10 @@ namespace ModelContextProtocol.Protocol; [JsonConverter(typeof(Converter))] public abstract class Reference { - private protected Reference() { } + /// Prevent external derivations. + private protected Reference() + { + } /// /// Gets or sets the type of content. @@ -87,6 +90,8 @@ public sealed class Converter : JsonConverter } } + // TODO: This converter exists due to the lack of downlevel support for AllowOutOfOrderMetadataProperties. + switch (type) { case "ref/prompt": @@ -95,7 +100,7 @@ public sealed class Converter : JsonConverter throw new JsonException("Prompt references must have a 'name' property."); } - return new PromptReference() { Name = name, Title = title }; + return new PromptReference { Name = name, Title = title }; case "ref/resource": if (uri is null) @@ -103,7 +108,7 @@ public sealed class Converter : JsonConverter throw new JsonException("Resource references must have a 'uri' property."); } - return new ResourceTemplateReference() { Uri = uri }; + return new ResourceTemplateReference { Uri = uri }; default: throw new JsonException($"Unknown content type: '{type}'"); diff --git a/src/ModelContextProtocol.Core/Protocol/RequestParams.cs b/src/ModelContextProtocol.Core/Protocol/RequestParams.cs index 6abf1d61..b45f55f7 100644 --- a/src/ModelContextProtocol.Core/Protocol/RequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/RequestParams.cs @@ -11,6 +11,11 @@ namespace ModelContextProtocol.Protocol; /// public abstract class RequestParams { + /// Prevent external derivations. + private protected RequestParams() + { + } + /// /// Gets or sets metadata reserved by MCP for protocol-level metadata. /// diff --git a/src/ModelContextProtocol.Core/Protocol/RequestParamsMetadata.cs b/src/ModelContextProtocol.Core/Protocol/RequestParamsMetadata.cs index 90f56218..963584cc 100644 --- a/src/ModelContextProtocol.Core/Protocol/RequestParamsMetadata.cs +++ b/src/ModelContextProtocol.Core/Protocol/RequestParamsMetadata.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol; /// This class contains properties that are used by the Model Context Protocol /// for features like progress tracking and other protocol-specific capabilities. /// -public class RequestParamsMetadata +public sealed class RequestParamsMetadata { /// /// Gets or sets an opaque token that will be attached to any subsequent progress notifications. diff --git a/src/ModelContextProtocol.Core/Protocol/Resource.cs b/src/ModelContextProtocol.Core/Protocol/Resource.cs index 01cc5446..63dce7fd 100644 --- a/src/ModelContextProtocol.Core/Protocol/Resource.cs +++ b/src/ModelContextProtocol.Core/Protocol/Resource.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class Resource : IBaseMetadata +public sealed class Resource : IBaseMetadata { /// [JsonPropertyName("name")] diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs b/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs index 6e6754f1..d5f25f41 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceContents.cs @@ -29,6 +29,7 @@ namespace ModelContextProtocol.Protocol; [JsonConverter(typeof(Converter))] public abstract class ResourceContents { + /// Prevent external derivations. private protected ResourceContents() { } diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceListChangedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/ResourceListChangedNotificationParams.cs index d481ee47..611fc9da 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceListChangedNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceListChangedNotificationParams.cs @@ -12,4 +12,4 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ResourceListChangedNotificationParams : NotificationParams; +public sealed class ResourceListChangedNotificationParams : NotificationParams; diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs b/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs index 3f5d19ba..d2959d18 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceTemplate.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Protocol; /// Resource templates provide metadata about resources available on the server, /// including how to construct URIs for those resources. /// -public class ResourceTemplate : IBaseMetadata +public sealed class ResourceTemplate : IBaseMetadata { /// [JsonPropertyName("name")] diff --git a/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs index cf48dca0..d97eaa75 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourceUpdatedNotificationParams.cs @@ -17,7 +17,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ResourceUpdatedNotificationParams : NotificationParams +public sealed class ResourceUpdatedNotificationParams : NotificationParams { /// /// Gets or sets the URI of the resource that was updated. diff --git a/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs b/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs index 12343cc7..1332a6aa 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol; /// /// See the schema for details. /// -public class ResourcesCapability +public sealed class ResourcesCapability { /// /// Gets or sets whether this server supports subscribing to resource updates. diff --git a/src/ModelContextProtocol.Core/Protocol/Result.cs b/src/ModelContextProtocol.Core/Protocol/Result.cs index 407bd96e..4e8393e9 100644 --- a/src/ModelContextProtocol.Core/Protocol/Result.cs +++ b/src/ModelContextProtocol.Core/Protocol/Result.cs @@ -8,6 +8,11 @@ namespace ModelContextProtocol.Protocol; /// public abstract class Result { + /// Prevent external derivations. + private protected Result() + { + } + /// /// Gets or sets metadata reserved by MCP for protocol-level metadata. /// diff --git a/src/ModelContextProtocol.Core/Protocol/Root.cs b/src/ModelContextProtocol.Core/Protocol/Root.cs index bca6836c..5893bfec 100644 --- a/src/ModelContextProtocol.Core/Protocol/Root.cs +++ b/src/ModelContextProtocol.Core/Protocol/Root.cs @@ -12,7 +12,7 @@ namespace ModelContextProtocol.Protocol; /// Roots provide a hierarchical structure for organizing and accessing resources within the protocol. /// Each root has a URI that uniquely identifies it and optional metadata like a human-readable name. /// -public class Root +public sealed class Root { /// /// Gets or sets the URI of the root. diff --git a/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs b/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs index e63c19c3..60d20b94 100644 --- a/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs @@ -19,7 +19,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class RootsCapability +public sealed class RootsCapability { /// /// Gets or sets whether the client supports notifications for changes to the roots list. diff --git a/src/ModelContextProtocol.Core/Protocol/RootsListChangedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/RootsListChangedNotificationParams.cs index f156e0b5..2e96a712 100644 --- a/src/ModelContextProtocol.Core/Protocol/RootsListChangedNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/RootsListChangedNotificationParams.cs @@ -12,4 +12,4 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class RootsListChangedNotificationParams : NotificationParams; +public sealed class RootsListChangedNotificationParams : NotificationParams; diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs b/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs index 2f5705c4..6e0f1190 100644 --- a/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs @@ -16,7 +16,7 @@ namespace ModelContextProtocol.Protocol; /// using an AI model. The client must set a to process these requests. /// /// -public class SamplingCapability +public sealed class SamplingCapability { // Currently empty in the spec, but may be extended in the future diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs b/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs index ca50b74c..4c4971cb 100644 --- a/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs +++ b/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs @@ -23,7 +23,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class SamplingMessage +public sealed class SamplingMessage { /// /// Gets or sets the content of the message. diff --git a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs index 1548241e..6a4b2e62 100644 --- a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs @@ -14,7 +14,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ServerCapabilities +public sealed class ServerCapabilities { /// /// Gets or sets experimental, non-standard capabilities that the server supports. @@ -32,7 +32,7 @@ public class ServerCapabilities /// /// [JsonPropertyName("experimental")] - public Dictionary? Experimental { get; set; } + public IDictionary? Experimental { get; set; } /// /// Gets or sets a server's logging capability, supporting sending log messages to the client. diff --git a/src/ModelContextProtocol.Core/Protocol/SetLevelRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/SetLevelRequestParams.cs index e95360a1..5346c9a5 100644 --- a/src/ModelContextProtocol.Core/Protocol/SetLevelRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/SetLevelRequestParams.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Protocol; /// This request allows clients to configure the level of logging information they want to receive from the server. /// The server will send notifications for log events at the specified level and all higher (more severe) levels. /// -public class SetLevelRequestParams : RequestParams +public sealed class SetLevelRequestParams : RequestParams { /// /// Gets or sets the level of logging that the client wants to receive from the server. diff --git a/src/ModelContextProtocol.Core/Protocol/SubscribeRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/SubscribeRequestParams.cs index ac49512c..24a92860 100644 --- a/src/ModelContextProtocol.Core/Protocol/SubscribeRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/SubscribeRequestParams.cs @@ -21,7 +21,7 @@ namespace ModelContextProtocol.Protocol; /// The server may refuse or limit subscriptions based on its capabilities or resource constraints. /// /// -public class SubscribeRequestParams : RequestParams +public sealed class SubscribeRequestParams : RequestParams { /// /// Gets or sets the URI of the resource to subscribe to. diff --git a/src/ModelContextProtocol.Core/Protocol/TextResourceContents.cs b/src/ModelContextProtocol.Core/Protocol/TextResourceContents.cs index 754b8c29..4659e15e 100644 --- a/src/ModelContextProtocol.Core/Protocol/TextResourceContents.cs +++ b/src/ModelContextProtocol.Core/Protocol/TextResourceContents.cs @@ -19,7 +19,7 @@ namespace ModelContextProtocol.Protocol; /// See the schema for more details. /// /// -public class TextResourceContents : ResourceContents +public sealed class TextResourceContents : ResourceContents { /// /// Gets or sets the text of the item. diff --git a/src/ModelContextProtocol.Core/Protocol/Tool.cs b/src/ModelContextProtocol.Core/Protocol/Tool.cs index 02b3c906..c09598ca 100644 --- a/src/ModelContextProtocol.Core/Protocol/Tool.cs +++ b/src/ModelContextProtocol.Core/Protocol/Tool.cs @@ -7,7 +7,7 @@ namespace ModelContextProtocol.Protocol; /// /// Represents a tool that the server is capable of calling. /// -public class Tool : IBaseMetadata +public sealed class Tool : IBaseMetadata { /// [JsonPropertyName("name")] diff --git a/src/ModelContextProtocol.Core/Protocol/ToolAnnotations.cs b/src/ModelContextProtocol.Core/Protocol/ToolAnnotations.cs index fbbc6ab5..3cb8b4f3 100644 --- a/src/ModelContextProtocol.Core/Protocol/ToolAnnotations.cs +++ b/src/ModelContextProtocol.Core/Protocol/ToolAnnotations.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Protocol; /// They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like `title`). /// Clients should never make tool use decisions based on received from untrusted servers. /// -public class ToolAnnotations +public sealed class ToolAnnotations { /// /// Gets or sets a human-readable title for the tool that can be displayed to users. diff --git a/src/ModelContextProtocol.Core/Protocol/ToolListChangedNotificationParams.cs b/src/ModelContextProtocol.Core/Protocol/ToolListChangedNotificationParams.cs index 0efb69c8..3cfac21e 100644 --- a/src/ModelContextProtocol.Core/Protocol/ToolListChangedNotificationParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ToolListChangedNotificationParams.cs @@ -12,4 +12,4 @@ namespace ModelContextProtocol.Protocol; /// See the schema for details. /// /// -public class ToolListChangedNotificationParams : NotificationParams; +public sealed class ToolListChangedNotificationParams : NotificationParams; diff --git a/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs b/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs index d6f474ea..5a3bec5c 100644 --- a/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs @@ -7,7 +7,7 @@ namespace ModelContextProtocol.Protocol; /// Represents the tools capability configuration. /// See the schema for details. /// -public class ToolsCapability +public sealed class ToolsCapability { /// /// Gets or sets whether this server supports notifications for changes to the tool list. diff --git a/src/ModelContextProtocol.Core/Protocol/UnsubscribeRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/UnsubscribeRequestParams.cs index 62117f08..49414f8c 100644 --- a/src/ModelContextProtocol.Core/Protocol/UnsubscribeRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/UnsubscribeRequestParams.cs @@ -18,7 +18,7 @@ namespace ModelContextProtocol.Protocol; /// for the same resource without causing errors, even if there is no active subscription. /// /// -public class UnsubscribeRequestParams : RequestParams +public sealed class UnsubscribeRequestParams : RequestParams { /// /// The URI of the resource to unsubscribe from. The URI can use any protocol; it is up to the server how to interpret it. diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs index b62ce2e9..8d446c58 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs @@ -250,7 +250,7 @@ public override async ValueTask GetAsync( string text => new() { Description = ProtocolPrompt.Description, - Messages = [new() { Role = Role.User, Content = new TextContentBlock() { Text = text } }], + Messages = [new() { Role = Role.User, Content = new TextContentBlock { Text = text } }], }, PromptMessage promptMessage => new() diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs index 86e34fc9..487fed74 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs @@ -398,17 +398,17 @@ private AIFunctionMcpServerResource(AIFunction function, ResourceTemplate resour TextContent tc => new() { - Contents = [new TextResourceContents() { Uri = request.Params!.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = tc.Text }], + Contents = [new TextResourceContents { Uri = request.Params!.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = tc.Text }], }, DataContent dc => new() { - Contents = [new BlobResourceContents() { Uri = request.Params!.Uri, MimeType = dc.MediaType, Blob = dc.Base64Data.ToString() }], + Contents = [new BlobResourceContents { Uri = request.Params!.Uri, MimeType = dc.MediaType, Blob = dc.Base64Data.ToString() }], }, string text => new() { - Contents = [new TextResourceContents() { Uri = request.Params!.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = text }], + Contents = [new TextResourceContents { Uri = request.Params!.Uri, MimeType = ProtocolResourceTemplate.MimeType, Text = text }], }, IEnumerable contents => new() diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index 08c610f6..39dd19e3 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -299,7 +299,7 @@ public override async ValueTask InvokeAsync( return new() { IsError = true, - Content = [new TextContentBlock() { Text = errorMessage }], + Content = [new TextContentBlock { Text = errorMessage }], }; } @@ -321,7 +321,7 @@ public override async ValueTask InvokeAsync( string text => new() { - Content = [new TextContentBlock() { Text = text }], + Content = [new TextContentBlock { Text = text }], StructuredContent = structuredContent, }, @@ -333,7 +333,7 @@ public override async ValueTask InvokeAsync( IEnumerable texts => new() { - Content = [.. texts.Select(x => new TextContentBlock() { Text = x ?? string.Empty })], + Content = [.. texts.Select(x => new TextContentBlock { Text = x ?? string.Empty })], StructuredContent = structuredContent, }, @@ -349,7 +349,7 @@ public override async ValueTask InvokeAsync( _ => new() { - Content = [new TextContentBlock() { Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))) }], + Content = [new TextContentBlock { Text = JsonSerializer.Serialize(result, AIFunction.JsonSerializerOptions.GetTypeInfo(typeof(object))) }], StructuredContent = structuredContent, }, }; diff --git a/src/ModelContextProtocol.Core/Server/McpServer.cs b/src/ModelContextProtocol.Core/Server/McpServer.cs index 81473da0..829e0a86 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.cs @@ -242,7 +242,13 @@ await originalListResourcesHandler(request, cancellationToken).ConfigureAwait(fa if (request.Params?.Cursor is null) { - result.Resources.AddRange(resources.Select(t => t.ProtocolResource).OfType()); + foreach (var r in resources) + { + if (r.ProtocolResource is { } resource) + { + result.Resources.Add(resource); + } + } } return result; @@ -257,7 +263,13 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure if (request.Params?.Cursor is null) { - result.ResourceTemplates.AddRange(resources.Where(t => t.IsTemplated).Select(t => t.ProtocolResourceTemplate)); + foreach (var rt in resources) + { + if (rt.IsTemplated) + { + result.ResourceTemplates.Add(rt.ProtocolResourceTemplate); + } + } } return result; @@ -366,7 +378,10 @@ await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(fals if (request.Params?.Cursor is null) { - result.Prompts.AddRange(prompts.Select(t => t.ProtocolPrompt)); + foreach (var p in prompts) + { + result.Prompts.Add(p.ProtocolPrompt); + } } return result; @@ -431,7 +446,10 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false) if (request.Params?.Cursor is null) { - result.Tools.AddRange(tools.Select(t => t.ProtocolTool)); + foreach (var t in tools) + { + result.Tools.Add(t.ProtocolTool); + } } return result; diff --git a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs index 6178d5a9..1b435c6a 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs @@ -94,7 +94,7 @@ public static async Task SampleAsync( samplingMessages.Add(new() { Role = role, - Content = new TextContentBlock() { Text = textContent.Text }, + Content = new TextContentBlock { Text = textContent.Text }, }); break; diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs index e84a012c..8c50a9b5 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs @@ -5,7 +5,7 @@ namespace ModelContextProtocol.Server; /// /// Provides configuration options for the MCP server. /// -public class McpServerOptions +public sealed class McpServerOptions { /// /// Gets or sets information about this server implementation, including its name and version. diff --git a/tests/Common/Utils/TestServerTransport.cs b/tests/Common/Utils/TestServerTransport.cs index 70b77f44..f875fe50 100644 --- a/tests/Common/Utils/TestServerTransport.cs +++ b/tests/Common/Utils/TestServerTransport.cs @@ -12,7 +12,7 @@ public class TestServerTransport : ITransport public ChannelReader MessageReader => _messageChannel; - public List SentMessages { get; } = []; + public IList SentMessages { get; } = []; public Action? OnMessageSent { get; set; } @@ -74,7 +74,7 @@ private async Task SamplingAsync(JsonRpcRequest request, CancellationToken cance await WriteMessageAsync(new JsonRpcResponse { Id = request.Id, - Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new TextContentBlock() { Text = "" }, Model = "model", Role = Role.User }, McpJsonUtilities.DefaultOptions), + Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new TextContentBlock { Text = "" }, Model = "model", Role = Role.User }, McpJsonUtilities.DefaultOptions), }, cancellationToken); } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs index cd434d7c..4537d16b 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs @@ -199,7 +199,7 @@ public async Task AdditionalHeaders_AreSent_InGetAndPostRequests() { Endpoint = new Uri("http://localhost/sse"), Name = "In-memory SSE Client", - AdditionalHeaders = new() + AdditionalHeaders = new Dictionary { ["Authorize"] = "Bearer testToken" }, @@ -226,7 +226,7 @@ public async Task EmptyAdditionalHeadersKey_Throws_InvalidOperationException() { Endpoint = new Uri("http://localhost/sse"), Name = "In-memory SSE Client", - AdditionalHeaders = new() + AdditionalHeaders = new Dictionary() { [""] = "" }, diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs index 116bdc40..c5711020 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs @@ -79,7 +79,7 @@ private async Task StartAsync() Id = request.Id, Result = JsonSerializer.SerializeToNode(new CallToolResult() { - Content = [new TextContentBlock() { Text = parameters.Arguments["message"].ToString() }], + Content = [new TextContentBlock { Text = parameters.Arguments["message"].ToString() }], }, McpJsonUtilities.DefaultOptions), }); } diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs index 9325fdc1..97013f64 100644 --- a/tests/ModelContextProtocol.TestServer/Program.cs +++ b/tests/ModelContextProtocol.TestServer/Program.cs @@ -177,14 +177,14 @@ private static ToolsCapability ConfigureTools() } return new CallToolResult() { - Content = [new TextContentBlock() { Text = $"Echo: {message}" }] + Content = [new TextContentBlock { Text = $"Echo: {message}" }] }; } else if (request.Params?.Name == "echoSessionId") { return new CallToolResult() { - Content = [new TextContentBlock() { Text = request.Server.SessionId ?? string.Empty }] + Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }] }; } else if (request.Params?.Name == "sampleLLM") @@ -200,7 +200,7 @@ private static ToolsCapability ConfigureTools() return new CallToolResult() { - Content = [new TextContentBlock() { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] + Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] }; } else @@ -257,7 +257,7 @@ private static PromptsCapability ConfigurePrompts() messages.Add(new PromptMessage() { Role = Role.User, - Content = new TextContentBlock() { Text = "This is a simple prompt without arguments." }, + Content = new TextContentBlock { Text = "This is a simple prompt without arguments." }, }); } else if (request.Params?.Name == "complex_prompt") @@ -267,12 +267,12 @@ private static PromptsCapability ConfigurePrompts() messages.Add(new PromptMessage() { Role = Role.User, - Content = new TextContentBlock() { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, + Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, }); messages.Add(new PromptMessage() { Role = Role.Assistant, - Content = new TextContentBlock() { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, + Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, }); messages.Add(new PromptMessage() { @@ -499,19 +499,19 @@ private static CompletionsCapability ConfigureCompletions() case ResourceTemplateReference rtr: var resourceId = rtr.Uri?.Split('/').LastOrDefault(); if (string.IsNullOrEmpty(resourceId)) - return new CompleteResult() { Completion = new() { Values = [] } }; + return new CompleteResult { Completion = new() { Values = [] } }; // Filter resource IDs that start with the input value values = sampleResourceIds.Where(id => id.StartsWith(request.Params!.Argument.Value)).ToArray(); - return new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } }; + return new CompleteResult { Completion = new() { Values = values, HasMore = false, Total = values.Length } }; case PromptReference pr: // Handle completion for prompt arguments if (!exampleCompletions.TryGetValue(request.Params.Argument.Name, out var completions)) - return new CompleteResult() { Completion = new() { Values = [] } }; + return new CompleteResult { Completion = new() { Values = [] } }; values = completions.Where(value => value.StartsWith(request.Params.Argument.Value)).ToArray(); - return new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } }; + return new CompleteResult { Completion = new() { Values = values, HasMore = false, Total = values.Length } }; default: throw new McpException($"Unknown reference type: '{request.Params?.Ref.Type}'", McpErrorCode.InvalidParams); @@ -528,7 +528,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st Messages = [new SamplingMessage() { Role = Role.User, - Content = new TextContentBlock() { Text = $"Resource {uri} context: {context}" }, + Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs index d81666da..56e98c98 100644 --- a/tests/ModelContextProtocol.TestSseServer/Program.cs +++ b/tests/ModelContextProtocol.TestSseServer/Program.cs @@ -46,7 +46,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st Messages = [new SamplingMessage() { Role = Role.User, - Content = new TextContentBlock() { Text = $"Resource {uri} context: {context}" }, + Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, @@ -171,14 +171,14 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st } return new CallToolResult() { - Content = [new TextContentBlock() { Text = $"Echo: {message}" }] + Content = [new TextContentBlock { Text = $"Echo: {message}" }] }; } else if (request.Params.Name == "echoSessionId") { return new CallToolResult() { - Content = [new TextContentBlock() { Text = request.Server.SessionId ?? string.Empty }] + Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }] }; } else if (request.Params.Name == "sampleLLM") @@ -194,7 +194,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st return new CallToolResult() { - Content = [new TextContentBlock() { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] + Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] }; } else @@ -304,8 +304,8 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st { Name = "complex_prompt", Description = "A prompt with arguments", - Arguments = new() - { + Arguments = + [ new PromptArgument() { Name = "temperature", @@ -318,7 +318,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st Description = "Output style", Required = false } - } + ], } ] }; @@ -335,7 +335,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st messages.Add(new PromptMessage() { Role = Role.User, - Content = new TextContentBlock() { Text = "This is a simple prompt without arguments." }, + Content = new TextContentBlock { Text = "This is a simple prompt without arguments." }, }); } else if (request.Params.Name == "complex_prompt") @@ -345,12 +345,12 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st messages.Add(new PromptMessage() { Role = Role.User, - Content = new TextContentBlock() { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, + Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, }); messages.Add(new PromptMessage() { Role = Role.Assistant, - Content = new TextContentBlock() { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, + Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, }); messages.Add(new PromptMessage() { diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs index 634afa9f..7e66fa3d 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs @@ -17,7 +17,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer mcpServerBuilder.WithReadResourceHandler((request, cancellationToken) => new ValueTask(new ReadResourceResult() { - Contents = [new TextResourceContents() { Text = request.Params?.Uri ?? string.Empty }] + Contents = [new TextResourceContents { Text = request.Params?.Uri ?? string.Empty }] })); } @@ -95,10 +95,10 @@ public class TestGroup public int Level { get; set; } = 4; [JsonPropertyName("variables")] - public Dictionary Variables { get; set; } = []; + public IDictionary Variables { get; set; } = new Dictionary(); [JsonPropertyName("testcases")] - public List> TestCases { get; set; } = []; + public IList> TestCases { get; set; } = []; } [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)] diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index f98fa1ad..11ee7eaf 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -71,7 +71,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer case "FinalCustomPrompt": return new GetPromptResult() { - Messages = [new() { Role = Role.User, Content = new TextContentBlock() { Text = $"hello from {request.Params.Name}" } }], + Messages = [new() { Role = Role.User, Content = new TextContentBlock { Text = $"hello from {request.Params.Name}" } }], }; default: @@ -287,10 +287,10 @@ public sealed class MorePrompts { [McpServerPrompt] public static PromptMessage AnotherPrompt(ObjectWithId id) => - new PromptMessage + new() { Role = Role.User, - Content = new TextContentBlock() { Text = "hello" }, + Content = new TextContentBlock { Text = "hello" }, }; } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs index ef0eebe8..7cee174d 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs @@ -101,7 +101,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer case "test://ResourceTemplate2": return new ReadResourceResult() { - Contents = [new TextResourceContents() { Text = request.Params?.Uri ?? "(null)" }] + Contents = [new TextResourceContents { Text = request.Params?.Uri ?? "(null)" }] }; } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index 1fb7bcc7..99740712 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -97,7 +97,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer case "FinalCustomTool": return new CallToolResult() { - Content = [new TextContentBlock() { Text = $"{request.Params.Name}Result" }], + Content = [new TextContentBlock { Text = $"{request.Params.Name}Result" }], }; default: diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs index ef0fbf16..8f90d256 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs @@ -16,7 +16,7 @@ public McpServerScopedTests(ITestOutputHelper testOutputHelper) protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) { mcpServerBuilder.WithTools(serializerOptions: McpServerScopedTestsJsonContext.Default.Options); - services.AddScoped(_ => new ComplexObject() { Name = "Scoped" }); + services.AddScoped(_ => new ComplexObject { Name = "Scoped" }); } [Fact] diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs index c3287829..ca1bfe97 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs @@ -56,7 +56,7 @@ public async Task SupportsServiceFromDI() McpServerPrompt prompt = McpServerPrompt.Create((MyService actualMyService, int? something = null) => { Assert.Same(expectedMyService, actualMyService); - return new PromptMessage() { Role = Role.Assistant, Content = new TextContentBlock() { Text = "Hello" } }; + return new PromptMessage { Role = Role.Assistant, Content = new TextContentBlock { Text = "Hello" } }; }, new() { Services = services }); Assert.Contains("something", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []); @@ -84,7 +84,7 @@ public async Task SupportsOptionalServiceFromDI() McpServerPrompt prompt = McpServerPrompt.Create((MyService? actualMyService = null) => { Assert.Null(actualMyService); - return new PromptMessage() { Role = Role.Assistant, Content = new TextContentBlock() { Text = "Hello" } }; + return new PromptMessage { Role = Role.Assistant, Content = new TextContentBlock { Text = "Hello" } }; }, new() { Services = services }); var result = await prompt.GetAsync( @@ -177,7 +177,7 @@ public async Task CanReturnPromptMessage() PromptMessage expected = new() { Role = Role.User, - Content = new TextContentBlock() { Text = "hello" } + Content = new TextContentBlock { Text = "hello" } }; McpServerPrompt prompt = McpServerPrompt.Create(() => @@ -198,16 +198,17 @@ public async Task CanReturnPromptMessage() [Fact] public async Task CanReturnPromptMessages() { - PromptMessage[] expected = [ + IList expected = + [ new() { Role = Role.User, - Content = new TextContentBlock() { Text = "hello" } + Content = new TextContentBlock { Text = "hello" } }, new() { Role = Role.Assistant, - Content = new TextContentBlock() { Text = "hello again" } + Content = new TextContentBlock { Text = "hello again" } } ]; @@ -235,7 +236,7 @@ public async Task CanReturnChatMessage() PromptMessage expected = new() { Role = Role.User, - Content = new TextContentBlock() { Text = "hello" } + Content = new TextContentBlock { Text = "hello" } }; McpServerPrompt prompt = McpServerPrompt.Create(() => @@ -261,12 +262,12 @@ public async Task CanReturnChatMessages() new() { Role = Role.User, - Content = new TextContentBlock() { Text = "hello" } + Content = new TextContentBlock { Text = "hello" } }, new() { Role = Role.Assistant, - Content = new TextContentBlock() { Text = "hello again" } + Content = new TextContentBlock { Text = "hello again" } } ]; @@ -347,8 +348,9 @@ private sealed class MyService; private class DisposablePromptType : IDisposable { + private readonly ChatMessage _message = new(ChatRole.User, ""); + public int Disposals { get; private set; } - private ChatMessage _message = new ChatMessage(ChatRole.User, ""); public void Dispose() { @@ -370,7 +372,7 @@ public ChatMessage InstanceMethod() private class AsyncDisposablePromptType : IAsyncDisposable { public int AsyncDisposals { get; private set; } - private ChatMessage _message = new ChatMessage(ChatRole.User, ""); + private ChatMessage _message = new(ChatRole.User, ""); public ValueTask DisposeAsync() { @@ -392,9 +394,10 @@ public ChatMessage InstanceMethod() private class AsyncDisposableAndDisposablePromptType : IAsyncDisposable, IDisposable { + private readonly ChatMessage _message = new(ChatRole.User, ""); + public int Disposals { get; private set; } public int AsyncDisposals { get; private set; } - private ChatMessage _message = new ChatMessage(ChatRole.User, ""); public void Dispose() { diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs index 6e43c293..3e25d4e9 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs @@ -433,7 +433,7 @@ public async Task CanReturnReadResult() McpServerResource resource = McpServerResource.Create((IMcpServer server) => { Assert.Same(mockServer.Object, server); - return new ReadResourceResult() { Contents = new List() { new TextResourceContents() { Text = "hello" } } }; + return new ReadResourceResult { Contents = new List { new TextResourceContents { Text = "hello" } } }; }, new() { Name = "Test" }); var result = await resource.ReadAsync( new RequestContext(mockServer.Object) { Params = new() { Uri = "resource://Test" } }, @@ -450,7 +450,7 @@ public async Task CanReturnResourceContents() McpServerResource resource = McpServerResource.Create((IMcpServer server) => { Assert.Same(mockServer.Object, server); - return new TextResourceContents() { Text = "hello" }; + return new TextResourceContents { Text = "hello" }; }, new() { Name = "Test", SerializerOptions = JsonContext6.Default.Options }); var result = await resource.ReadAsync( new RequestContext(mockServer.Object) { Params = new() { Uri = "resource://Test" } }, @@ -467,11 +467,11 @@ public async Task CanReturnCollectionOfResourceContents() McpServerResource resource = McpServerResource.Create((IMcpServer server) => { Assert.Same(mockServer.Object, server); - return new List() - { - new TextResourceContents() { Text = "hello" }, - new BlobResourceContents() { Blob = Convert.ToBase64String(new byte[] { 1, 2, 3 }) }, - }; + return (IList) + [ + new TextResourceContents { Text = "hello" }, + new BlobResourceContents { Blob = Convert.ToBase64String(new byte[] { 1, 2, 3 }) }, + ]; }, new() { Name = "Test" }); var result = await resource.ReadAsync( new RequestContext(mockServer.Object) { Params = new() { Uri = "resource://Test" } }, @@ -506,7 +506,7 @@ public async Task CanReturnCollectionOfStrings() McpServerResource resource = McpServerResource.Create((IMcpServer server) => { Assert.Same(mockServer.Object, server); - return new List() { "42", "43" }; + return new List { "42", "43" }; }, new() { Name = "Test", SerializerOptions = JsonContext6.Default.Options }); var result = await resource.ReadAsync( new RequestContext(mockServer.Object) { Params = new() { Uri = "resource://Test" } }, diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index d72efa72..9e7eca9f 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -635,7 +635,7 @@ public Task SendRequestAsync(JsonRpcRequest request, Cancellati CreateMessageResult result = new() { - Content = new TextContentBlock() { Text = "The Eiffel Tower." }, + Content = new TextContentBlock { Text = "The Eiffel Tower." }, Model = "amazingmodel", Role = Role.Assistant, StopReason = "endTurn", diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs index 24260b97..74213341 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs @@ -185,7 +185,7 @@ public async Task CanReturnCollectionOfAIContent() McpServerTool tool = McpServerTool.Create((IMcpServer server) => { Assert.Same(mockServer.Object, server); - return new List() { + return new List { new TextContent("text"), new DataContent("data:image/png;base64,1234"), new DataContent("data:audio/wav;base64,1234") @@ -291,7 +291,7 @@ public async Task CanReturnCollectionOfStrings() McpServerTool tool = McpServerTool.Create((IMcpServer server) => { Assert.Same(mockServer.Object, server); - return new List() { "42", "43" }; + return new List { "42", "43" }; }, new() { SerializerOptions = JsonContext2.Default.Options }); var result = await tool.InvokeAsync( new RequestContext(mockServer.Object), @@ -325,7 +325,11 @@ public async Task CanReturnCollectionOfMcpContent() McpServerTool tool = McpServerTool.Create((IMcpServer server) => { Assert.Same(mockServer.Object, server); - return new List() { new TextContentBlock() { Text = "42" }, new ImageContentBlock() { Data = "1234", MimeType = "image/png" } }; + return (IList) + [ + new TextContentBlock { Text = "42" }, + new ImageContentBlock { Data = "1234", MimeType = "image/png" } + ]; }); var result = await tool.InvokeAsync( new RequestContext(mockServer.Object), @@ -341,7 +345,7 @@ public async Task CanReturnCallToolResult() { CallToolResult response = new() { - Content = new List() { new TextContentBlock { Text = "text" }, new ImageContentBlock { Data = "1234", MimeType = "image/png" } } + Content = new List { new TextContentBlock { Text = "text" }, new ImageContentBlock { Data = "1234", MimeType = "image/png" } } }; Mock mockServer = new(); @@ -405,7 +409,7 @@ public async Task ToolCallError_LogsErrorMessage() var mockServer = new Mock(); var request = new RequestContext(mockServer.Object) { - Params = new CallToolRequestParams() { Name = toolName }, + Params = new CallToolRequestParams { Name = toolName }, Services = serviceProvider }; @@ -505,7 +509,7 @@ public static IEnumerable StructuredOutput_ReturnsExpectedSchema_Input yield return new object[] { 42 }; yield return new object[] { 3.14 }; yield return new object[] { true }; - yield return new object[] { new object() }; + yield return new object[] { new() }; yield return new object[] { new List { "item1", "item2" } }; yield return new object[] { new Dictionary { ["key1"] = 1, ["key2"] = 2 } }; yield return new object[] { new Person("John", 27) }; diff --git a/tests/ModelContextProtocol.Tests/Transport/SseClientTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/SseClientTransportTests.cs index ae449ac9..cc6b4e0a 100644 --- a/tests/ModelContextProtocol.Tests/Transport/SseClientTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/SseClientTransportTests.cs @@ -116,7 +116,7 @@ public async Task SendMessageAsync_Handles_Accepted_Response() }; await using var session = await transport.ConnectAsync(TestContext.Current.CancellationToken); - await session.SendMessageAsync(new JsonRpcRequest() { Method = RequestMethods.Initialize, Id = new RequestId(44) }, CancellationToken.None); + await session.SendMessageAsync(new JsonRpcRequest { Method = RequestMethods.Initialize, Id = new RequestId(44) }, CancellationToken.None); Assert.True(true); }