From 33fdb21f4381a486805cd0dc93120b6ac93efa73 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 27 Mar 2025 21:40:46 -0400 Subject: [PATCH 1/2] Add ToolAnnotations support --- .../Tools/SampleLlmTool.cs | 2 +- .../Tools/SampleLlmTool.cs | 2 +- .../AIContentExtensions.cs | 4 +- .../Client/McpClientExtensions.cs | 6 +- .../Client/McpClientTool.cs | 14 +- .../McpServerBuilderExtensions.Tools.cs | 26 +-- .../Transport/StdioServerTransport.cs | 2 +- .../Protocol/Types/Annotated.cs | 2 +- .../Protocol/Types/Annotations.cs | 2 +- .../Protocol/Types/Argument.cs | 2 +- .../Protocol/Types/CallToolRequestParams.cs | 2 +- .../Protocol/Types/CallToolResponse.cs | 2 +- .../Protocol/Types/Capabilities.cs | 14 +- .../Protocol/Types/CompleteRequestParams.cs | 2 +- .../Protocol/Types/CompleteResult.cs | 2 +- .../Protocol/Types/Completion.cs | 2 +- .../Protocol/Types/Content.cs | 4 +- .../Protocol/Types/ContextInclusion.cs | 2 +- .../Types/CreateMessageRequestParams.cs | 2 +- .../Protocol/Types/CreateMessageResult.cs | 2 +- .../Protocol/Types/EmptyResult.cs | 2 +- .../Protocol/Types/GetPromptRequestParams.cs | 2 +- .../Protocol/Types/GetPromptResult.cs | 2 +- .../Protocol/Types/Implementation.cs | 2 +- .../Protocol/Types/InitializeRequestParams.cs | 2 +- .../Protocol/Types/InitializeResult.cs | 2 +- .../Types/ListPromptsRequestParams.cs | 2 +- .../Protocol/Types/ListPromptsResult.cs | 2 +- .../ListResourceTemplatesRequestParams.cs | 2 +- .../Types/ListResourceTemplatesResult.cs | 2 +- .../Types/ListResourcesRequestParams.cs | 2 +- .../Protocol/Types/ListResourcesResult.cs | 2 +- .../Protocol/Types/ListRootsRequestParams.cs | 2 +- .../Protocol/Types/ListRootsResult.cs | 2 +- .../Protocol/Types/ListToolsRequestParams.cs | 2 +- .../Protocol/Types/ListToolsResult.cs | 2 +- .../Types/LoggingMessageNotificationParams.cs | 2 +- .../Protocol/Types/ModelHint.cs | 2 +- .../Protocol/Types/ModelPreferences.cs | 2 +- .../Protocol/Types/PingResult.cs | 2 +- .../Protocol/Types/Prompt.cs | 2 +- .../Protocol/Types/PromptArgument.cs | 2 +- .../Protocol/Types/PromptMessage.cs | 2 +- .../Types/ReadResourceRequestParams.cs | 2 +- .../Protocol/Types/ReadResourceResult.cs | 2 +- .../Protocol/Types/Reference.cs | 2 +- .../Protocol/Types/RequestParams.cs | 2 +- .../Protocol/Types/Resource.cs | 2 +- .../Protocol/Types/ResourceContents.cs | 2 +- .../Protocol/Types/ResourceTemplate.cs | 2 +- .../ResourceUpdatedNotificationParams.cs | 2 +- .../Protocol/Types/Role.cs | 2 +- .../Protocol/Types/Root.cs | 2 +- .../Protocol/Types/SamplingMessage.cs | 2 +- .../Protocol/Types/ServerCapabilities.cs | 2 +- .../Protocol/Types/SetLevelRequestParams.cs | 2 +- .../Protocol/Types/SubscribeRequestParams.cs | 2 +- .../Protocol/Types/Tool.cs | 8 +- .../Protocol/Types/ToolAnnotations.cs | 54 +++++ .../UnsubscribeFromResourceRequestParams.cs | 2 +- .../Types/UnsubscribeRequestParams.cs | 2 +- .../Server/AIFunctionMcpServerTool.cs | 125 ++++++++--- .../Server/McpServerExtensions.cs | 4 +- .../Server/McpServerTool.cs | 73 ++----- .../Server/McpServerToolAttribute.cs | 62 +++++- .../Server/McpServerToolCreateOptions.cs | 76 +++++++ .../Client/McpClientExtensionsTests.cs | 24 ++- .../McpServerBuilderExtensionsToolsTests.cs | 44 +++- .../Server/McpServerToolReturnTests.cs | 194 ----------------- .../Server/McpServerToolTests.cs | 195 +++++++++++++++++- 70 files changed, 640 insertions(+), 393 deletions(-) create mode 100644 src/ModelContextProtocol/Protocol/Types/ToolAnnotations.cs create mode 100644 src/ModelContextProtocol/Server/McpServerToolCreateOptions.cs delete mode 100644 tests/ModelContextProtocol.Tests/Server/McpServerToolReturnTests.cs diff --git a/samples/AspNetCoreSseServer/Tools/SampleLlmTool.cs b/samples/AspNetCoreSseServer/Tools/SampleLlmTool.cs index 880787ed3..4f175a453 100644 --- a/samples/AspNetCoreSseServer/Tools/SampleLlmTool.cs +++ b/samples/AspNetCoreSseServer/Tools/SampleLlmTool.cs @@ -10,7 +10,7 @@ namespace TestServerWithHosting.Tools; [McpServerToolType] public static class SampleLlmTool { - [McpServerTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] + [McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] public static async Task SampleLLM( IMcpServer thisServer, [Description("The prompt to send to the LLM")] string prompt, diff --git a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs index 62ed56ec0..b1a0353d4 100644 --- a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs +++ b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs @@ -10,7 +10,7 @@ namespace TestServerWithHosting.Tools; [McpServerToolType] public static class SampleLlmTool { - [McpServerTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] + [McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] public static async Task SampleLLM( IMcpServer thisServer, [Description("The prompt to send to the LLM")] string prompt, diff --git a/src/ModelContextProtocol/AIContentExtensions.cs b/src/ModelContextProtocol/AIContentExtensions.cs index 87f40daaa..be5eabe2f 100644 --- a/src/ModelContextProtocol/AIContentExtensions.cs +++ b/src/ModelContextProtocol/AIContentExtensions.cs @@ -33,7 +33,7 @@ public static AIContent ToAIContent(this Content content) Throw.IfNull(content); AIContent ac; - if (content is { Type: "image", MimeType: not null, Data: not null }) + if (content is { Type: "image" or "audio", MimeType: not null, Data: not null }) { ac = new DataContent(Convert.FromBase64String(content.Data), content.MimeType); } @@ -112,6 +112,7 @@ internal static Content ToContent(this AIContent content) => Text = textContent.Text, Type = "text", }, + DataContent dataContent => new() { Data = dataContent.GetBase64Data(), @@ -121,6 +122,7 @@ internal static Content ToContent(this AIContent content) => dataContent.HasTopLevelMediaType("audio") ? "audio" : "resource", }, + _ => new() { Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))), diff --git a/src/ModelContextProtocol/Client/McpClientExtensions.cs b/src/ModelContextProtocol/Client/McpClientExtensions.cs index bf453cd91..ced728e7a 100644 --- a/src/ModelContextProtocol/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol/Client/McpClientExtensions.cs @@ -469,7 +469,7 @@ internal static (IList Messages, ChatOptions? Options) ToChatClient { message.Contents.Add(new TextContent(sm.Content.Text)); } - else if (sm.Content is { Type: "image", MimeType: not null, Data: not null }) + 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)); } @@ -512,11 +512,11 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat { foreach (var lmc in lastMessage.Contents) { - if (lmc is DataContent dc && dc.HasTopLevelMediaType("image")) + if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio"))) { content = new() { - Type = "image", + Type = dc.HasTopLevelMediaType("image") ? "image" : "audio", MimeType = dc.MediaType, Data = dc.GetBase64Data(), }; diff --git a/src/ModelContextProtocol/Client/McpClientTool.cs b/src/ModelContextProtocol/Client/McpClientTool.cs index a2bb172a9..10ccd81a6 100644 --- a/src/ModelContextProtocol/Client/McpClientTool.cs +++ b/src/ModelContextProtocol/Client/McpClientTool.cs @@ -9,22 +9,24 @@ namespace ModelContextProtocol.Client; public sealed class McpClientTool : AIFunction { private readonly IMcpClient _client; - private readonly Tool _tool; internal McpClientTool(IMcpClient client, Tool tool) { _client = client; - _tool = tool; + ProtocolTool = tool; } + /// Gets the protocol type for this instance. + public Tool ProtocolTool { get; } + /// - public override string Name => _tool.Name; + public override string Name => ProtocolTool.Name; /// - public override string Description => _tool.Description ?? string.Empty; + public override string Description => ProtocolTool.Description ?? string.Empty; /// - public override JsonElement JsonSchema => _tool.InputSchema; + public override JsonElement JsonSchema => ProtocolTool.InputSchema; /// public override JsonSerializerOptions JsonSerializerOptions => McpJsonUtilities.DefaultOptions; @@ -37,7 +39,7 @@ internal McpClientTool(IMcpClient client, Tool tool) arguments as IReadOnlyDictionary ?? arguments.ToDictionary(); - CallToolResponse result = await _client.CallToolAsync(_tool.Name, argDict, cancellationToken).ConfigureAwait(false); + CallToolResponse result = await _client.CallToolAsync(ProtocolTool.Name, argDict, cancellationToken).ConfigureAwait(false); return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse); } } \ No newline at end of file diff --git a/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Tools.cs b/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Tools.cs index 75d2cb7da..8b63e20b8 100644 --- a/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Tools.cs +++ b/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Tools.cs @@ -37,14 +37,9 @@ public static partial class McpServerBuilderExtensions { if (toolMethod.GetCustomAttribute() is not null) { - if (toolMethod.IsStatic) - { - builder.Services.AddSingleton(services => McpServerTool.Create(toolMethod, services: services)); - } - else - { - builder.Services.AddSingleton(services => McpServerTool.Create(toolMethod, typeof(TTool), services: services)); - } + builder.Services.AddSingleton((Func)(toolMethod.IsStatic ? + services => McpServerTool.Create(toolMethod, new McpServerToolCreateOptions() { Services = services }) : + services => McpServerTool.Create(toolMethod, typeof(TTool), new() { Services = services }))); } } @@ -71,18 +66,13 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params { if (toolType is not null) { - foreach (var method in toolType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) + foreach (var toolMethod in toolType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) { - if (method.GetCustomAttribute() is not null) + if (toolMethod.GetCustomAttribute() is not null) { - if (method.IsStatic) - { - builder.Services.AddSingleton(services => McpServerTool.Create(method, services: services)); - } - else - { - builder.Services.AddSingleton(services => McpServerTool.Create(method, toolType, services: services)); - } + builder.Services.AddSingleton((Func)(toolMethod.IsStatic ? + services => McpServerTool.Create(toolMethod, new McpServerToolCreateOptions() { Services = services }) : + services => McpServerTool.Create(toolMethod, toolType, new() { Services = services }))); } } } diff --git a/src/ModelContextProtocol/Protocol/Transport/StdioServerTransport.cs b/src/ModelContextProtocol/Protocol/Transport/StdioServerTransport.cs index e1f517e59..9eb6986c7 100644 --- a/src/ModelContextProtocol/Protocol/Transport/StdioServerTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/StdioServerTransport.cs @@ -25,7 +25,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport private readonly TextReader _stdInReader; private readonly Stream _stdOutStream; - private SemaphoreSlim _sendLock = new(1, 1); + private readonly SemaphoreSlim _sendLock = new(1, 1); private Task? _readTask; private CancellationTokenSource? _shutdownCts; diff --git a/src/ModelContextProtocol/Protocol/Types/Annotated.cs b/src/ModelContextProtocol/Protocol/Types/Annotated.cs index 2b3458ff4..3096c78c1 100644 --- a/src/ModelContextProtocol/Protocol/Types/Annotated.cs +++ b/src/ModelContextProtocol/Protocol/Types/Annotated.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Base for objects that include optional annotations for the client. The client can use annotations to inform how objects are used or displayed. -/// See the schema for details +/// See the schema for details /// public abstract record Annotated { diff --git a/src/ModelContextProtocol/Protocol/Types/Annotations.cs b/src/ModelContextProtocol/Protocol/Types/Annotations.cs index 906aebb59..af2ee6fdc 100644 --- a/src/ModelContextProtocol/Protocol/Types/Annotations.cs +++ b/src/ModelContextProtocol/Protocol/Types/Annotations.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Represents annotations that can be attached to content. -/// See the schema for details +/// See the schema for details /// public record Annotations { diff --git a/src/ModelContextProtocol/Protocol/Types/Argument.cs b/src/ModelContextProtocol/Protocol/Types/Argument.cs index 4fccd63dd..8f6d8ba1e 100644 --- a/src/ModelContextProtocol/Protocol/Types/Argument.cs +++ b/src/ModelContextProtocol/Protocol/Types/Argument.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Used for completion requests to provide additional context for the completion options. -/// See the schema for details +/// See the schema for details /// public class Argument { diff --git a/src/ModelContextProtocol/Protocol/Types/CallToolRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/CallToolRequestParams.cs index 3a9b6bd9f..b7c8d3f63 100644 --- a/src/ModelContextProtocol/Protocol/Types/CallToolRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/CallToolRequestParams.cs @@ -2,7 +2,7 @@ /// /// Used by the client to invoke a tool provided by the server. -/// See the schema for details +/// See the schema for details /// public class CallToolRequestParams : RequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/CallToolResponse.cs b/src/ModelContextProtocol/Protocol/Types/CallToolResponse.cs index 27024eb45..c25db36e1 100644 --- a/src/ModelContextProtocol/Protocol/Types/CallToolResponse.cs +++ b/src/ModelContextProtocol/Protocol/Types/CallToolResponse.cs @@ -11,7 +11,7 @@ /// However, any errors in _finding_ the tool, an error indicating that the /// server does not support tool calls, or any other exceptional conditions, /// should be reported as an MCP error response. -/// See the schema for details +/// See the schema for details /// public class CallToolResponse { diff --git a/src/ModelContextProtocol/Protocol/Types/Capabilities.cs b/src/ModelContextProtocol/Protocol/Types/Capabilities.cs index 9be4fdd59..b3f758edc 100644 --- a/src/ModelContextProtocol/Protocol/Types/Capabilities.cs +++ b/src/ModelContextProtocol/Protocol/Types/Capabilities.cs @@ -5,7 +5,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Represents the capabilities that a client may support. -/// See the schema for details +/// See the schema for details /// public class ClientCapabilities { @@ -30,7 +30,7 @@ public class ClientCapabilities /// /// Represents the roots capability configuration. -/// See the schema for details +/// See the schema for details /// public class RootsCapability { @@ -47,7 +47,7 @@ public class RootsCapability /// /// Represents the sampling capability configuration. -/// See the schema for details +/// See the schema for details /// public class SamplingCapability { @@ -60,7 +60,7 @@ public class SamplingCapability /// /// Represents the logging capability configuration. -/// See the schema for details +/// See the schema for details /// public class LoggingCapability { @@ -76,7 +76,7 @@ public class LoggingCapability /// /// Represents the prompts capability configuration. -/// See the schema for details +/// See the schema for details /// public class PromptsCapability { @@ -101,7 +101,7 @@ public class PromptsCapability /// /// Represents the resources capability configuration. -/// See the schema for details +/// See the schema for details /// public class ResourcesCapability { @@ -150,7 +150,7 @@ public class ResourcesCapability /// /// Represents the tools capability configuration. -/// See the schema for details +/// See the schema for details /// public class ToolsCapability { diff --git a/src/ModelContextProtocol/Protocol/Types/CompleteRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/CompleteRequestParams.cs index eea139da4..431146f3a 100644 --- a/src/ModelContextProtocol/Protocol/Types/CompleteRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/CompleteRequestParams.cs @@ -2,7 +2,7 @@ /// /// A request from the client to the server, to ask for completion options. -/// See the schema for details +/// See the schema for details /// public class CompleteRequestParams : RequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/CompleteResult.cs b/src/ModelContextProtocol/Protocol/Types/CompleteResult.cs index 392326d75..4bd77c814 100644 --- a/src/ModelContextProtocol/Protocol/Types/CompleteResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/CompleteResult.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// The server's response to a completion/complete request -/// See the schema for details +/// See the schema for details /// public class CompleteResult { diff --git a/src/ModelContextProtocol/Protocol/Types/Completion.cs b/src/ModelContextProtocol/Protocol/Types/Completion.cs index d53f98937..51d1344fa 100644 --- a/src/ModelContextProtocol/Protocol/Types/Completion.cs +++ b/src/ModelContextProtocol/Protocol/Types/Completion.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Represents a completion object in the server's response -/// See the schema for details +/// See the schema for details /// public class Completion { diff --git a/src/ModelContextProtocol/Protocol/Types/Content.cs b/src/ModelContextProtocol/Protocol/Types/Content.cs index 79229b962..71bc475db 100644 --- a/src/ModelContextProtocol/Protocol/Types/Content.cs +++ b/src/ModelContextProtocol/Protocol/Types/Content.cs @@ -4,13 +4,13 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Represents the content of a tool response. -/// See the schema for details +/// See the schema for details /// There are multiple subtypes of content, depending on the "type" field, these are represented as separate classes. /// public class Content { /// - /// The type of content. This determines the structure of the content object. Can be "image", "text", "resource". + /// The type of content. This determines the structure of the content object. Can be "image", "audio", "text", "resource". /// [JsonPropertyName("type")] diff --git a/src/ModelContextProtocol/Protocol/Types/ContextInclusion.cs b/src/ModelContextProtocol/Protocol/Types/ContextInclusion.cs index a513e4d7c..db45dd48f 100644 --- a/src/ModelContextProtocol/Protocol/Types/ContextInclusion.cs +++ b/src/ModelContextProtocol/Protocol/Types/ContextInclusion.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. -/// See the schema for details +/// See the schema for details /// [JsonConverter(typeof(JsonStringEnumConverter))] public enum ContextInclusion diff --git a/src/ModelContextProtocol/Protocol/Types/CreateMessageRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/CreateMessageRequestParams.cs index 329e33dbb..aa38e2178 100644 --- a/src/ModelContextProtocol/Protocol/Types/CreateMessageRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/CreateMessageRequestParams.cs @@ -7,7 +7,7 @@ /// /// While these align with the protocol specification, /// clients have full discretion over model selection and should inform users before sampling. -/// See the schema for details +/// See the schema for details /// public class CreateMessageRequestParams : RequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/CreateMessageResult.cs b/src/ModelContextProtocol/Protocol/Types/CreateMessageResult.cs index 843e03df6..f2b0795c4 100644 --- a/src/ModelContextProtocol/Protocol/Types/CreateMessageResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/CreateMessageResult.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol.Types; /// The client's response to a sampling/create_message request from the server. /// The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) /// and decide whether to allow the server to see it. -/// See the schema for details +/// See the schema for details /// public class CreateMessageResult { diff --git a/src/ModelContextProtocol/Protocol/Types/EmptyResult.cs b/src/ModelContextProtocol/Protocol/Types/EmptyResult.cs index 7e858ff1c..3dc1a8de8 100644 --- a/src/ModelContextProtocol/Protocol/Types/EmptyResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/EmptyResult.cs @@ -2,7 +2,7 @@ /// /// An empty result object. -/// See the schema for details +/// See the schema for details /// public class EmptyResult { diff --git a/src/ModelContextProtocol/Protocol/Types/GetPromptRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/GetPromptRequestParams.cs index 6b05930a3..08207667b 100644 --- a/src/ModelContextProtocol/Protocol/Types/GetPromptRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/GetPromptRequestParams.cs @@ -2,7 +2,7 @@ /// /// Used by the client to get a prompt provided by the server. -/// See the schema for details +/// See the schema for details /// public class GetPromptRequestParams : RequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/GetPromptResult.cs b/src/ModelContextProtocol/Protocol/Types/GetPromptResult.cs index bdf388670..80c61a5ca 100644 --- a/src/ModelContextProtocol/Protocol/Types/GetPromptResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/GetPromptResult.cs @@ -2,7 +2,7 @@ /// /// The server's response to a prompts/get request from the client. -/// See the schema for details +/// See the schema for details /// public class GetPromptResult { diff --git a/src/ModelContextProtocol/Protocol/Types/Implementation.cs b/src/ModelContextProtocol/Protocol/Types/Implementation.cs index 81fe8c015..73037bf4f 100644 --- a/src/ModelContextProtocol/Protocol/Types/Implementation.cs +++ b/src/ModelContextProtocol/Protocol/Types/Implementation.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Describes the name and version of an MCP implementation. -/// See the schema for details +/// See the schema for details /// public class Implementation { diff --git a/src/ModelContextProtocol/Protocol/Types/InitializeRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/InitializeRequestParams.cs index 5790c4fc6..d99f679eb 100644 --- a/src/ModelContextProtocol/Protocol/Types/InitializeRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/InitializeRequestParams.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Parameters for an initialization request sent to the server. -/// See the schema for details +/// See the schema for details /// public class InitializeRequestParams : RequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/InitializeResult.cs b/src/ModelContextProtocol/Protocol/Types/InitializeResult.cs index 475e33b69..717ed35cb 100644 --- a/src/ModelContextProtocol/Protocol/Types/InitializeResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/InitializeResult.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Result of the initialization request sent to the server. -/// See the schema for details +/// See the schema for details /// public record InitializeResult { diff --git a/src/ModelContextProtocol/Protocol/Types/ListPromptsRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/ListPromptsRequestParams.cs index 5f19789f0..419b6fceb 100644 --- a/src/ModelContextProtocol/Protocol/Types/ListPromptsRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/ListPromptsRequestParams.cs @@ -2,7 +2,7 @@ /// /// Sent from the client to request a list of prompts and prompt templates the server has. -/// See the schema for details +/// See the schema for details /// public class ListPromptsRequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/ListPromptsResult.cs b/src/ModelContextProtocol/Protocol/Types/ListPromptsResult.cs index 108df740f..ac592f315 100644 --- a/src/ModelContextProtocol/Protocol/Types/ListPromptsResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/ListPromptsResult.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// The server's response to a prompts/list request from the client. -/// See the schema for details +/// See the schema for details /// public class ListPromptsResult : PaginatedResult { diff --git a/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesRequestParams.cs index 4b4eecf90..f4060dbd0 100644 --- a/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesRequestParams.cs @@ -2,7 +2,7 @@ /// /// Sent from the client to request a list of resource templates the server has. -/// See the schema for details +/// See the schema for details /// public class ListResourceTemplatesRequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesResult.cs b/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesResult.cs index d2337923b..b437e3852 100644 --- a/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesResult.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// The server's response to a resources/templates/list request from the client. -/// See the schema for details +/// See the schema for details /// public class ListResourceTemplatesResult : PaginatedResult { diff --git a/src/ModelContextProtocol/Protocol/Types/ListResourcesRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/ListResourcesRequestParams.cs index f0c36dc55..ad7f19b31 100644 --- a/src/ModelContextProtocol/Protocol/Types/ListResourcesRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/ListResourcesRequestParams.cs @@ -2,7 +2,7 @@ /// /// Sent from the client to request a list of resources the server has. -/// See the schema for details +/// See the schema for details /// public class ListResourcesRequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/ListResourcesResult.cs b/src/ModelContextProtocol/Protocol/Types/ListResourcesResult.cs index 255281535..a5a0e6c1e 100644 --- a/src/ModelContextProtocol/Protocol/Types/ListResourcesResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/ListResourcesResult.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// The server's response to a resources/list request from the client. -/// See the schema for details +/// See the schema for details /// public class ListResourcesResult : PaginatedResult { diff --git a/src/ModelContextProtocol/Protocol/Types/ListRootsRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/ListRootsRequestParams.cs index a71020901..23dbfd6a1 100644 --- a/src/ModelContextProtocol/Protocol/Types/ListRootsRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/ListRootsRequestParams.cs @@ -2,7 +2,7 @@ /// /// A request from the server to get a list of root URIs from the client. -/// See the schema for details +/// See the schema for details /// public class ListRootsRequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/ListRootsResult.cs b/src/ModelContextProtocol/Protocol/Types/ListRootsResult.cs index 3e5922312..ce7114fe7 100644 --- a/src/ModelContextProtocol/Protocol/Types/ListRootsResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/ListRootsResult.cs @@ -2,7 +2,7 @@ /// /// The client's response to a roots/list request from the server. -/// See the schema for details +/// See the schema for details /// public class ListRootsResult { diff --git a/src/ModelContextProtocol/Protocol/Types/ListToolsRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/ListToolsRequestParams.cs index 631c78829..4f18fbb73 100644 --- a/src/ModelContextProtocol/Protocol/Types/ListToolsRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/ListToolsRequestParams.cs @@ -2,7 +2,7 @@ /// /// Sent from the client to request a list of tools the server has. -/// See the schema for details +/// See the schema for details /// public class ListToolsRequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/ListToolsResult.cs b/src/ModelContextProtocol/Protocol/Types/ListToolsResult.cs index 552b189a6..f27791062 100644 --- a/src/ModelContextProtocol/Protocol/Types/ListToolsResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/ListToolsResult.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// A response to a request to list the tools available on the server. -/// See the schema for details +/// See the schema for details /// public class ListToolsResult : PaginatedResult { diff --git a/src/ModelContextProtocol/Protocol/Types/LoggingMessageNotificationParams.cs b/src/ModelContextProtocol/Protocol/Types/LoggingMessageNotificationParams.cs index 2784bbcb3..8c153f254 100644 --- a/src/ModelContextProtocol/Protocol/Types/LoggingMessageNotificationParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/LoggingMessageNotificationParams.cs @@ -7,7 +7,7 @@ namespace ModelContextProtocol.Protocol.Types; /// Sent from the server as the payload of "notifications/message" notifications whenever a log message is generated. /// /// If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. -/// See the schema for details +/// See the schema for details /// public class LoggingMessageNotificationParams { diff --git a/src/ModelContextProtocol/Protocol/Types/ModelHint.cs b/src/ModelContextProtocol/Protocol/Types/ModelHint.cs index b30b5e261..262cd8614 100644 --- a/src/ModelContextProtocol/Protocol/Types/ModelHint.cs +++ b/src/ModelContextProtocol/Protocol/Types/ModelHint.cs @@ -4,7 +4,7 @@ /// Hints to use for model selection. /// Keys not declared here are currently left unspecified by the spec and are up /// to the client to interpret. -/// See the schema for details +/// See the schema for details /// public class ModelHint { diff --git a/src/ModelContextProtocol/Protocol/Types/ModelPreferences.cs b/src/ModelContextProtocol/Protocol/Types/ModelPreferences.cs index 303fbb9e0..96420f3ba 100644 --- a/src/ModelContextProtocol/Protocol/Types/ModelPreferences.cs +++ b/src/ModelContextProtocol/Protocol/Types/ModelPreferences.cs @@ -11,7 +11,7 @@ /// These preferences are always advisory. The client MAY ignore them. It is also /// up to the client to decide how to interpret these preferences and how to /// balance them against other considerations. -/// See the schema for details +/// See the schema for details /// public class ModelPreferences { diff --git a/src/ModelContextProtocol/Protocol/Types/PingResult.cs b/src/ModelContextProtocol/Protocol/Types/PingResult.cs index d7afb4f8c..c6b0c493d 100644 --- a/src/ModelContextProtocol/Protocol/Types/PingResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/PingResult.cs @@ -2,7 +2,7 @@ /// /// Dummy result for the ping request. -/// See the schema for details +/// See the schema for details /// public record PingResult { diff --git a/src/ModelContextProtocol/Protocol/Types/Prompt.cs b/src/ModelContextProtocol/Protocol/Types/Prompt.cs index a8bfee025..90ee447ac 100644 --- a/src/ModelContextProtocol/Protocol/Types/Prompt.cs +++ b/src/ModelContextProtocol/Protocol/Types/Prompt.cs @@ -2,7 +2,7 @@ /// /// A prompt or prompt template that the server offers. -/// See the schema for details +/// See the schema for details /// public class Prompt { diff --git a/src/ModelContextProtocol/Protocol/Types/PromptArgument.cs b/src/ModelContextProtocol/Protocol/Types/PromptArgument.cs index b80e08f0d..fbe26e9b2 100644 --- a/src/ModelContextProtocol/Protocol/Types/PromptArgument.cs +++ b/src/ModelContextProtocol/Protocol/Types/PromptArgument.cs @@ -2,7 +2,7 @@ /// /// Describes an argument that a prompt can accept. -/// See the schema for details +/// See the schema for details /// public class PromptArgument { diff --git a/src/ModelContextProtocol/Protocol/Types/PromptMessage.cs b/src/ModelContextProtocol/Protocol/Types/PromptMessage.cs index eacd6f047..611a8b45b 100644 --- a/src/ModelContextProtocol/Protocol/Types/PromptMessage.cs +++ b/src/ModelContextProtocol/Protocol/Types/PromptMessage.cs @@ -5,7 +5,7 @@ /// /// This is similar to `SamplingMessage`, but also supports the embedding of /// resources from the MCP server. -/// See the schema for details +/// See the schema for details /// public class PromptMessage { diff --git a/src/ModelContextProtocol/Protocol/Types/ReadResourceRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/ReadResourceRequestParams.cs index bf345d73b..d8f9b5630 100644 --- a/src/ModelContextProtocol/Protocol/Types/ReadResourceRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/ReadResourceRequestParams.cs @@ -2,7 +2,7 @@ /// /// Sent from the client to the server, to read a specific resource URI. -/// See the schema for details +/// See the schema for details /// public class ReadResourceRequestParams : RequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/ReadResourceResult.cs b/src/ModelContextProtocol/Protocol/Types/ReadResourceResult.cs index 9ea35e17a..c317b0c3c 100644 --- a/src/ModelContextProtocol/Protocol/Types/ReadResourceResult.cs +++ b/src/ModelContextProtocol/Protocol/Types/ReadResourceResult.cs @@ -2,7 +2,7 @@ /// /// The server's response to a resources/read request from the client. -/// See the schema for details +/// See the schema for details /// public class ReadResourceResult { diff --git a/src/ModelContextProtocol/Protocol/Types/Reference.cs b/src/ModelContextProtocol/Protocol/Types/Reference.cs index 9ea2ea082..bc84c5406 100644 --- a/src/ModelContextProtocol/Protocol/Types/Reference.cs +++ b/src/ModelContextProtocol/Protocol/Types/Reference.cs @@ -5,7 +5,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Represents a reference to a resource or prompt. Umbrella type for both ResourceReference and PromptReference from the spec schema. -/// See the schema for details +/// See the schema for details /// public class Reference { diff --git a/src/ModelContextProtocol/Protocol/Types/RequestParams.cs b/src/ModelContextProtocol/Protocol/Types/RequestParams.cs index 1185bd080..a3de78d66 100644 --- a/src/ModelContextProtocol/Protocol/Types/RequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/RequestParams.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Base class for all request parameters. -/// See the schema for details +/// See the schema for details /// public abstract class RequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/Resource.cs b/src/ModelContextProtocol/Protocol/Types/Resource.cs index fbe44bce7..d80ac94c5 100644 --- a/src/ModelContextProtocol/Protocol/Types/Resource.cs +++ b/src/ModelContextProtocol/Protocol/Types/Resource.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Represents a known resource that the server is capable of reading. -/// See the schema for details +/// See the schema for details /// public record Resource : Annotated { diff --git a/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs b/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs index 89b34ab47..4dc524a2c 100644 --- a/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs +++ b/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Represents the content of a resource. -/// See the schema for details +/// See the schema for details /// public class ResourceContents { diff --git a/src/ModelContextProtocol/Protocol/Types/ResourceTemplate.cs b/src/ModelContextProtocol/Protocol/Types/ResourceTemplate.cs index f5731188d..1df0c7eda 100644 --- a/src/ModelContextProtocol/Protocol/Types/ResourceTemplate.cs +++ b/src/ModelContextProtocol/Protocol/Types/ResourceTemplate.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Represents a known resource template that the server is capable of reading. -/// See the schema for details +/// See the schema for details /// public record ResourceTemplate : Annotated { diff --git a/src/ModelContextProtocol/Protocol/Types/ResourceUpdatedNotificationParams.cs b/src/ModelContextProtocol/Protocol/Types/ResourceUpdatedNotificationParams.cs index 5df7b1b1b..8317293c0 100644 --- a/src/ModelContextProtocol/Protocol/Types/ResourceUpdatedNotificationParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/ResourceUpdatedNotificationParams.cs @@ -2,7 +2,7 @@ /// /// Sent from the server as the payload of "notifications/resources/updated" notifications whenever a subscribed resource changes. -/// See the schema for details +/// See the schema for details /// public class ResourceUpdatedNotificationParams { diff --git a/src/ModelContextProtocol/Protocol/Types/Role.cs b/src/ModelContextProtocol/Protocol/Types/Role.cs index 91c9899ae..1cb35ea5b 100644 --- a/src/ModelContextProtocol/Protocol/Types/Role.cs +++ b/src/ModelContextProtocol/Protocol/Types/Role.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Represents the type of role in the conversation. -/// See the schema for details +/// See the schema for details /// [JsonConverter(typeof(JsonStringEnumConverter))] public enum Role diff --git a/src/ModelContextProtocol/Protocol/Types/Root.cs b/src/ModelContextProtocol/Protocol/Types/Root.cs index 1fa96fb25..90ad57650 100644 --- a/src/ModelContextProtocol/Protocol/Types/Root.cs +++ b/src/ModelContextProtocol/Protocol/Types/Root.cs @@ -2,7 +2,7 @@ /// /// Represents a root URI and its metadata. -/// See the schema for details +/// See the schema for details /// public class Root { diff --git a/src/ModelContextProtocol/Protocol/Types/SamplingMessage.cs b/src/ModelContextProtocol/Protocol/Types/SamplingMessage.cs index b3ee6f15c..c43ffe0bd 100644 --- a/src/ModelContextProtocol/Protocol/Types/SamplingMessage.cs +++ b/src/ModelContextProtocol/Protocol/Types/SamplingMessage.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Describes a message issued to or received from an LLM API. -/// See the schema for details +/// See the schema for details /// public class SamplingMessage { diff --git a/src/ModelContextProtocol/Protocol/Types/ServerCapabilities.cs b/src/ModelContextProtocol/Protocol/Types/ServerCapabilities.cs index 6890674c5..296e4e62b 100644 --- a/src/ModelContextProtocol/Protocol/Types/ServerCapabilities.cs +++ b/src/ModelContextProtocol/Protocol/Types/ServerCapabilities.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Represents the capabilities that a server may support. -/// See the schema for details +/// See the schema for details /// public class ServerCapabilities { diff --git a/src/ModelContextProtocol/Protocol/Types/SetLevelRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/SetLevelRequestParams.cs index bdc987ab8..c9f2586c4 100644 --- a/src/ModelContextProtocol/Protocol/Types/SetLevelRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/SetLevelRequestParams.cs @@ -4,7 +4,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// A request from the client to the server, to enable or adjust logging. -/// See the schema for details +/// See the schema for details /// public class SetLevelRequestParams : RequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/SubscribeRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/SubscribeRequestParams.cs index e0963679c..36fe47f22 100644 --- a/src/ModelContextProtocol/Protocol/Types/SubscribeRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/SubscribeRequestParams.cs @@ -2,7 +2,7 @@ /// /// Sent from the client to request updated notifications from the server whenever a particular primitive changes. -/// See the schema for details +/// See the schema for details /// public class SubscribeRequestParams : RequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/Tool.cs b/src/ModelContextProtocol/Protocol/Types/Tool.cs index 3349da15e..ed0c71290 100644 --- a/src/ModelContextProtocol/Protocol/Types/Tool.cs +++ b/src/ModelContextProtocol/Protocol/Types/Tool.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol.Protocol.Types; /// /// Represents a tool that the server is capable of calling. Part of the ListToolsResponse. -/// See the schema for details +/// See the schema for details /// public class Tool { @@ -44,4 +44,10 @@ public JsonElement InputSchema _inputSchema = value; } } + + /// + /// Optional additional tool information. + /// + [JsonPropertyName("annotations")] + public ToolAnnotations? Annotations { get; set; } } diff --git a/src/ModelContextProtocol/Protocol/Types/ToolAnnotations.cs b/src/ModelContextProtocol/Protocol/Types/ToolAnnotations.cs new file mode 100644 index 000000000..67d933c94 --- /dev/null +++ b/src/ModelContextProtocol/Protocol/Types/ToolAnnotations.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; + +namespace ModelContextProtocol.Protocol.Types; + +/// +/// Additional properties describing a Tool to clients. +/// NOTE: all properties in ToolAnnotations are **hints**. +/// 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 ToolAnnotations received from untrusted servers. +/// See the schema for details +/// There are multiple subtypes of content, depending on the "type" field, these are represented as separate classes. +/// +public class ToolAnnotations +{ + /// + /// A human-readable title for the tool. + /// + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// + /// If true, the tool may perform destructive updates to its environment. + /// If false, the tool performs only additive updates. + /// (This property is meaningful only when is false). + /// Default: true. + /// + [JsonPropertyName("destructiveHint")] + public bool? DestructiveHint { get; set; } + + /// + /// If true, calling the tool repeatedly with the same arguments + /// will have no additional effect on its environment. + /// (This property is meaningful only when is false). + /// Default: false. + /// + [JsonPropertyName("idempotentHint")] + public bool? IdempotentHint { get; set; } + + /// + /// If true, this tool may interact with an "open world" of external entities. + /// If false, the tool's domain of interaction is closed. + /// For example, the world of a web search tool is open, whereas that of a memory tool is not. + /// Default: true. + /// + [JsonPropertyName("openWorldHint")] + public bool? OpenWorldHint { get; set; } + + /// + /// If true, the tool does not modify its environment. + /// Default: false. + /// + [JsonPropertyName("readOnlyHint")] + public bool? ReadOnlyHint { get; set; } +} \ No newline at end of file diff --git a/src/ModelContextProtocol/Protocol/Types/UnsubscribeFromResourceRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/UnsubscribeFromResourceRequestParams.cs index 0ac697f7c..59528ddfc 100644 --- a/src/ModelContextProtocol/Protocol/Types/UnsubscribeFromResourceRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/UnsubscribeFromResourceRequestParams.cs @@ -2,7 +2,7 @@ /// /// Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. -/// See the schema for details +/// See the schema for details /// public class UnsubscribeFromResourceRequestParams { diff --git a/src/ModelContextProtocol/Protocol/Types/UnsubscribeRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/UnsubscribeRequestParams.cs index b138c5bd1..0f6bee111 100644 --- a/src/ModelContextProtocol/Protocol/Types/UnsubscribeRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/UnsubscribeRequestParams.cs @@ -2,7 +2,7 @@ /// /// Sent from the client to request not receiving updated notifications from the server whenever a primitive resource changes. -/// See the schema for details +/// See the schema for details /// public class UnsubscribeRequestParams : RequestParams { diff --git a/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs index 56ad40410..353aa92e6 100644 --- a/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs @@ -21,13 +21,13 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool /// public static new AIFunctionMcpServerTool Create( Delegate method, - string? name, - string? description, - IServiceProvider? services) + McpServerToolCreateOptions? options) { Throw.IfNull(method); + + options = DeriveOptions(method.Method, options); - return Create(method.Method, method.Target, name, description, services); + return Create(method.Method, method.Target, options); } /// @@ -36,9 +36,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool public static new AIFunctionMcpServerTool Create( MethodInfo method, object? target, - string? name, - string? description, - IServiceProvider? services) + McpServerToolCreateOptions? options) { Throw.IfNull(method); @@ -49,7 +47,11 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool // AIFunctionFactory, delete the TemporaryXx types, and fix-up the mechanism by // which the arguments are passed. - return Create(TemporaryAIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, name, description, services))); + options = DeriveOptions(method, options); + + return Create( + TemporaryAIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, options)), + options); } /// @@ -58,21 +60,23 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool public static new AIFunctionMcpServerTool Create( MethodInfo method, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, - string? name = null, - string? description = null, - IServiceProvider? services = null) + McpServerToolCreateOptions? options) { Throw.IfNull(method); - return Create(TemporaryAIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, name, description, services))); + options = DeriveOptions(method, options); + + return Create( + TemporaryAIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, options)), + options); } private static TemporaryAIFunctionFactoryOptions CreateAIFunctionFactoryOptions( - MethodInfo method, string? name, string? description, IServiceProvider? services) => - new TemporaryAIFunctionFactoryOptions() + MethodInfo method, McpServerToolCreateOptions? options) => + new() { - Name = name ?? method.GetCustomAttribute()?.Name, - Description = description, + Name = options?.Name ?? method.GetCustomAttribute()?.Name, + Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => Task.FromResult(result), ConfigureParameterBinding = pi => { @@ -97,7 +101,7 @@ private static TemporaryAIFunctionFactoryOptions CreateAIFunctionFactoryOptions( // We assume that if the services used to create the tool support a particular type, // so too do the services associated with the server. This is the same basic assumption // made in ASP.NET. - if (services is not null && + if (options?.Services is { } services && services.GetService() is { } ispis && ispis.IsService(pi.ParameterType)) { @@ -139,26 +143,80 @@ private static TemporaryAIFunctionFactoryOptions CreateAIFunctionFactoryOptions( }; /// Creates an that wraps the specified . - public static new AIFunctionMcpServerTool Create(AIFunction function) + public static new AIFunctionMcpServerTool Create(AIFunction function, McpServerToolCreateOptions? options) { Throw.IfNull(function); - return new AIFunctionMcpServerTool(function); + Tool tool = new() + { + Name = options?.Name ?? function.Name, + Description = options?.Description ?? function.Description, + InputSchema = function.JsonSchema, + }; + + if (options is not null) + { + if (options.Title is not null || + options.Idempotent is not null || + options.Destructive is not null || + options.OpenWorld is not null || + options.ReadOnly is not null) + { + tool.Annotations = new() + { + Title = options?.Title, + IdempotentHint = options?.Idempotent, + DestructiveHint = options?.Destructive, + OpenWorldHint = options?.OpenWorld, + ReadOnlyHint = options?.ReadOnly, + }; + } + } + + return new AIFunctionMcpServerTool(function, tool); + } + + private static McpServerToolCreateOptions? DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options) + { + McpServerToolCreateOptions newOptions = options?.Clone() ?? new(); + + if (method.GetCustomAttribute() is { } attr) + { + newOptions.Name ??= attr.Name; + newOptions.Title ??= attr.Title; + + if (attr.DestructiveIsSet) + { + newOptions.Destructive ??= attr.Destructive; + } + + if (attr.IdempotentIsSet) + { + newOptions.Idempotent ??= attr.Idempotent; + } + + if (attr.OpenWorldIsSet) + { + newOptions.OpenWorld ??= attr.OpenWorld; + } + + if (attr.ReadOnlyIsSet) + { + newOptions.ReadOnly ??= attr.ReadOnly; + } + } + + return newOptions; } /// Gets the wrapped by this tool. internal AIFunction AIFunction { get; } /// Initializes a new instance of the class. - private AIFunctionMcpServerTool(AIFunction function) + private AIFunctionMcpServerTool(AIFunction function, Tool tool) { AIFunction = function; - ProtocolTool = new() - { - Name = function.Name, - Description = function.Description, - InputSchema = function.JsonSchema, - }; + ProtocolTool = tool; } /// @@ -201,30 +259,37 @@ public override async Task InvokeAsync( { Content = [aiContent.ToContent()] }, + null => new() { Content = [] }, + string text => new() { Content = [new() { Text = text, Type = "text" }] }, + Content content => new() { Content = [content] }, + IEnumerable texts => new() { Content = [.. texts.Select(x => new Content() { Type = "text", Text = x ?? string.Empty })] }, + IEnumerable contentItems => new() { Content = [.. contentItems.Select(static item => item.ToContent())] }, + IEnumerable contents => new() { Content = [.. contents] }, + CallToolResponse callToolResponse => callToolResponse, // TODO https://github.com/modelcontextprotocol/csharp-sdk/issues/69: @@ -232,10 +297,10 @@ public override async Task InvokeAsync( _ => new() { Content = [new() - { - Text = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))), - Type = "text" - }] + { + Text = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))), + Type = "text" + }] }, }; } diff --git a/src/ModelContextProtocol/Server/McpServerExtensions.cs b/src/ModelContextProtocol/Server/McpServerExtensions.cs index 73bd528dd..7bff56642 100644 --- a/src/ModelContextProtocol/Server/McpServerExtensions.cs +++ b/src/ModelContextProtocol/Server/McpServerExtensions.cs @@ -88,13 +88,13 @@ public static async Task RequestSamplingAsync( }); break; - case DataContent dataContent when dataContent.HasTopLevelMediaType("image"): + case DataContent dataContent when dataContent.HasTopLevelMediaType("image") || dataContent.HasTopLevelMediaType("audio"): samplingMessages.Add(new() { Role = role, Content = new() { - Type = "image", + Type = dataContent.HasTopLevelMediaType("image") ? "image" : "audio", MimeType = dataContent.MediaType, Data = dataContent.GetBase64Data(), }, diff --git a/src/ModelContextProtocol/Server/McpServerTool.cs b/src/ModelContextProtocol/Server/McpServerTool.cs index c262df75a..d1003f575 100644 --- a/src/ModelContextProtocol/Server/McpServerTool.cs +++ b/src/ModelContextProtocol/Server/McpServerTool.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.AI; using ModelContextProtocol.Protocol.Types; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -30,58 +29,28 @@ public abstract Task InvokeAsync( /// Creates an instance for a method, specified via a instance. /// /// The method to be represented via the created . - /// - /// The name to use for the . If , but an - /// is applied to , the name from the attribute will be used. If that's not present, the name based - /// on 's name will be used. - /// - /// - /// The description to use for the . If , but a - /// is applied to , the description from that attribute will be used. - /// - /// - /// Optional services used in the construction of the . These services will be - /// used to determine which parameters should be satisifed from dependency injection; what services - /// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time. - /// + /// Optional options used in the creation of the to control its behavior. /// The created for invoking . /// is . public static McpServerTool Create( Delegate method, - string? name = null, - string? description = null, - IServiceProvider? services = null) => - AIFunctionMcpServerTool.Create(method, name, description, services); + McpServerToolCreateOptions? options = null) => + AIFunctionMcpServerTool.Create(method, options); /// /// Creates an instance for a method, specified via a instance. /// /// The method to be represented via the created . /// The instance if is an instance method; otherwise, . - /// - /// The name to use for the . If , but an - /// is applied to , the name from the attribute will be used. If that's not present, the name based - /// on 's name will be used. - /// - /// - /// The description to use for the . If , but a - /// is applied to , the description from that attribute will be used. - /// - /// - /// Optional services used in the construction of the . These services will be - /// used to determine which parameters should be satisifed from dependency injection; what services - /// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time. - /// + /// Optional options used in the creation of the to control its behavior. /// The created for invoking . /// is . /// is an instance method but is . public static McpServerTool Create( MethodInfo method, object? target = null, - string? name = null, - string? description = null, - IServiceProvider? services = null) => - AIFunctionMcpServerTool.Create(method, target, name, description, services); + McpServerToolCreateOptions? options = null) => + AIFunctionMcpServerTool.Create(method, target, options); /// /// Creates an instance for a method, specified via an for @@ -96,39 +65,27 @@ public static McpServerTool Create( /// is used, utilizing the type's public parameterless constructor. /// If an instance can't be constructed, an exception is thrown during the function's invocation. /// - /// - /// The name to use for the . If , but an - /// is applied to , the name from the attribute will be used. If that's not present, the name based - /// on 's name will be used. - /// - /// - /// The description to use for the . If , but a - /// is applied to , the description from that attribute will be used. - /// - /// - /// Optional services used in the construction of the . These services will be - /// used to determine which parameters should be satisifed from dependency injection; what services - /// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time. - /// + /// Optional options used in the creation of the to control its behavior. /// The created for invoking . /// is . public static McpServerTool Create( MethodInfo method, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, - string? name = null, - string? description = null, - IServiceProvider? services = null) => - AIFunctionMcpServerTool.Create(method, targetType, name, description, services); + McpServerToolCreateOptions? options = null) => + AIFunctionMcpServerTool.Create(method, targetType, options); /// Creates an that wraps the specified . /// The function to wrap. + /// Optional options used in the creation of the to control its behavior. /// is . /// - /// Unlike the other overloads of Create, the created by + /// Unlike the other overloads of Create, the created by /// does not provide all of the special parameter handling for MCP-specific concepts, like . /// - public static McpServerTool Create(AIFunction function) => - AIFunctionMcpServerTool.Create(function); + public static McpServerTool Create( + AIFunction function, + McpServerToolCreateOptions? options = null) => + AIFunctionMcpServerTool.Create(function, options); /// public override string ToString() => ProtocolTool.Name; diff --git a/src/ModelContextProtocol/Server/McpServerToolAttribute.cs b/src/ModelContextProtocol/Server/McpServerToolAttribute.cs index d1489f6db..9ae52aa7d 100644 --- a/src/ModelContextProtocol/Server/McpServerToolAttribute.cs +++ b/src/ModelContextProtocol/Server/McpServerToolAttribute.cs @@ -1,18 +1,70 @@ namespace ModelContextProtocol.Server; /// -/// Used to mark a public method as an MCP tool. +/// Used to indicate that a method should be considered an MCP tool and describe it. /// [AttributeUsage(AttributeTargets.Method)] public sealed class McpServerToolAttribute : Attribute { + private bool? _destructive; + private bool? _idempotent; + private bool? _openWorld; + private bool? _readOnly; + + /// + /// Initializes a new instance of the class. + /// + public McpServerToolAttribute() + { + } + /// Gets the name of the tool. /// If , the method name will be used. - public string? Name { get; } + public string? Name { get; set; } /// - /// Initializes a new instance of the class. + /// Gets or sets a human-readable title for the tool. + /// + public string? Title { get; set; } + + /// + /// Gets or sets whether the tool may perform destructive updates to its environment. + /// + public bool Destructive { get => _destructive ?? true; set => _destructive = value; } + + /// + /// Gets whether the property has been assigned a value. + /// + public bool DestructiveIsSet => _destructive.HasValue; + + /// + /// Gets or sets whether calling the tool repeatedly with the same arguments will have no additional effect on its environment. + /// + public bool Idempotent { get => _idempotent ?? false; set => _idempotent = value; } + + /// + /// Gets whether the property has been assigned a value. + /// + public bool IdempotentIsSet => _idempotent.HasValue; + + /// + /// Gets or sets whether this tool may interact with an "open world" of external entities + /// (e.g. the world of a web search tool is open, whereas that of a memory tool is not). + /// + public bool OpenWorld { get => _openWorld ?? true; set => _openWorld = value; } + + /// + /// Gets whether the property has been assigned a value. + /// + public bool OpenWorldIsSet => _openWorld.HasValue; + + /// + /// Gets or sets whether the tool does not modify its environment. + /// + public bool ReadOnly { get => _readOnly ?? false; set => _readOnly = value; } + + /// + /// Gets whether the property has been assigned a value. /// - /// The name of the tool. If , the method name will be used. - public McpServerToolAttribute(string? name = null) => Name = name; + public bool ReadOnlyIsSet => _readOnly.HasValue; } diff --git a/src/ModelContextProtocol/Server/McpServerToolCreateOptions.cs b/src/ModelContextProtocol/Server/McpServerToolCreateOptions.cs new file mode 100644 index 000000000..51099fdc7 --- /dev/null +++ b/src/ModelContextProtocol/Server/McpServerToolCreateOptions.cs @@ -0,0 +1,76 @@ +using System.ComponentModel; + +namespace ModelContextProtocol.Server; + +/// Provides options for controlling the creation of an . +public sealed class McpServerToolCreateOptions +{ + /// + /// Gets or sets optional services used in the construction of the . + /// + /// + /// These services will be used to determine which parameters should be satisifed from dependency injection; what services + /// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time. + /// + public IServiceProvider? Services { get; set; } + + /// + /// Gets or sets the name to use for the . + /// + /// + /// If , but an is applied to the method, + /// the name from the attribute will be used. If that's not present, a name based on the method's name will be used. + /// + public string? Name { get; set; } + + /// + /// Gets or set the description to use for the . + /// + /// + /// If , but a is applied to the method, + /// the description from that attribute will be used. + /// + public string? Description { get; set; } + + /// + /// Gets or sets a human-readable title for the tool. + /// + public string? Title { get; set; } + + /// + /// Gets or sets whether the tool may perform destructive updates to its environment. + /// + public bool? Destructive { get; set; } + + /// + /// Gets or sets whether calling the tool repeatedly with the same arguments + /// will have no additional effect on its environment. + /// + public bool? Idempotent { get; set; } + + /// + /// Gets or sets whether this tool may interact with an "open world" of external entities. + /// + public bool? OpenWorld { get; set; } + + /// + /// Gets or sets whether this tool does not modify its environment. + /// + public bool? ReadOnly { get; set; } + + /// + /// Creates a shallow clone of the current instance. + /// + internal McpServerToolCreateOptions Clone() => + new McpServerToolCreateOptions() + { + Services = Services, + Name = Name, + Description = Description, + Title = Title, + Destructive = Destructive, + Idempotent = Idempotent, + OpenWorld = OpenWorld, + ReadOnly = ReadOnly + }; +} diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs index 03d0e9933..1a6bb8f84 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs @@ -10,8 +10,8 @@ namespace ModelContextProtocol.Tests.Client; public class McpClientExtensionsTests { - private Pipe _clientToServerPipe = new(); - private Pipe _serverToClientPipe = new(); + private readonly Pipe _clientToServerPipe = new(); + private readonly Pipe _serverToClientPipe = new(); private readonly IMcpServer _server; public McpClientExtensionsTests() @@ -22,8 +22,10 @@ public McpClientExtensionsTests() for (int f = 0; f < 10; f++) { string name = $"Method{f}"; - sc.AddSingleton(McpServerTool.Create((int i) => $"{name} Result {i}", name)); + sc.AddSingleton(McpServerTool.Create((int i) => $"{name} Result {i}", new() { Name = name })); } + sc.AddSingleton(McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)](string i) => $"{i} Result", new() { Name = "ValuesSetViaAttr" })); + sc.AddSingleton(McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)](string i) => $"{i} Result", new() { Name = "ValuesSetViaOptions", Destructive = true, OpenWorld = false, ReadOnly = true })); _server = sc.BuildServiceProvider().GetRequiredService(); } @@ -60,10 +62,24 @@ public async Task ListToolsAsync_AllToolsReturned() IMcpClient client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); - Assert.Equal(10, tools.Count); + Assert.Equal(12, tools.Count); var echo = tools.Single(t => t.Name == "Method4"); var result = await echo.InvokeAsync(new Dictionary() { ["i"] = 42 }, TestContext.Current.CancellationToken); Assert.Contains("Method4 Result 42", result?.ToString()); + + var valuesSetViaAttr = tools.Single(t => t.Name == "ValuesSetViaAttr"); + Assert.Null(valuesSetViaAttr.ProtocolTool.Annotations?.Title); + Assert.Null(valuesSetViaAttr.ProtocolTool.Annotations?.ReadOnlyHint); + Assert.Null(valuesSetViaAttr.ProtocolTool.Annotations?.IdempotentHint); + Assert.False(valuesSetViaAttr.ProtocolTool.Annotations?.DestructiveHint); + Assert.True(valuesSetViaAttr.ProtocolTool.Annotations?.OpenWorldHint); + + var valuesSetViaOptions = tools.Single(t => t.Name == "ValuesSetViaOptions"); + Assert.Null(valuesSetViaOptions.ProtocolTool.Annotations?.Title); + Assert.True(valuesSetViaOptions.ProtocolTool.Annotations?.ReadOnlyHint); + Assert.Null(valuesSetViaOptions.ProtocolTool.Annotations?.IdempotentHint); + Assert.True(valuesSetViaOptions.ProtocolTool.Annotations?.DestructiveHint); + Assert.False(valuesSetViaOptions.ProtocolTool.Annotations?.OpenWorldHint); } [Fact] diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index 3ae313010..f217b9b90 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -171,7 +171,7 @@ public async Task Can_Be_Notified_Of_Tool_Changes() var serverTools = _server.ServerOptions.Capabilities?.Tools?.ToolCollection; Assert.NotNull(serverTools); - var newTool = McpServerTool.Create([McpServerTool(name: "NewTool")] () => "42"); + var newTool = McpServerTool.Create([McpServerTool(Name = "NewTool")] () => "42"); serverTools.Add(newTool); await notificationRead; @@ -427,10 +427,42 @@ public void Register_Tools_From_Multiple_Sources() Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "MethodD"); } + [Fact] + public void Create_ExtractsToolAnnotations_AllSet() + { + var tool = McpServerTool.Create(EchoTool.ReturnInteger); + Assert.NotNull(tool); + Assert.NotNull(tool.ProtocolTool); + + var annotations = tool.ProtocolTool.Annotations; + Assert.NotNull(annotations); + Assert.Equal("Return An Integer", annotations.Title); + Assert.False(annotations.DestructiveHint); + Assert.True(annotations.IdempotentHint); + Assert.False(annotations.OpenWorldHint); + Assert.True(annotations.ReadOnlyHint); + } + + [Fact] + public void Create_ExtractsToolAnnotations_SomeSet() + { + var tool = McpServerTool.Create(EchoTool.ReturnJson); + Assert.NotNull(tool); + Assert.NotNull(tool.ProtocolTool); + + var annotations = tool.ProtocolTool.Annotations; + Assert.NotNull(annotations); + Assert.Null(annotations.Title); + Assert.Null(annotations.DestructiveHint); + Assert.False(annotations.IdempotentHint); + Assert.Null(annotations.OpenWorldHint); + Assert.Null(annotations.ReadOnlyHint); + } + [McpServerToolType] public sealed class EchoTool(ObjectWithId objectFromDI) { - private string _randomValue = Guid.NewGuid().ToString("N"); + private readonly string _randomValue = Guid.NewGuid().ToString("N"); [McpServerTool, Description("Echoes the input back to the client.")] public static string Echo([Description("the echoes message")] string message) @@ -438,7 +470,7 @@ public static string Echo([Description("the echoes message")] string message) return "hello " + message; } - [McpServerTool("double_echo"), Description("Echoes the input back to the client.")] + [McpServerTool(Name = "double_echo"), Description("Echoes the input back to the client.")] public static string Echo2(string message) { return "hello hello" + message; @@ -462,13 +494,13 @@ public static string[] EchoArray(string message) return null; } - [McpServerTool] + [McpServerTool(Idempotent = false)] public static JsonElement ReturnJson() { return JsonDocument.Parse("{\"SomeProp\": false}").RootElement; } - [McpServerTool] + [McpServerTool(Title = "Return An Integer", Destructive = false, Idempotent = true, OpenWorld = false, ReadOnly = true)] public static int ReturnInteger() { return 5; @@ -499,7 +531,7 @@ public static string EchoComplex(ComplexObject complex) [McpServerToolType] internal class AnotherToolType { - [McpServerTool("DifferentName")] + [McpServerTool(Name = "DifferentName")] private static string MethodA(int a) => a.ToString(); [McpServerTool] diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerToolReturnTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerToolReturnTests.cs deleted file mode 100644 index a263ab9c3..000000000 --- a/tests/ModelContextProtocol.Tests/Server/McpServerToolReturnTests.cs +++ /dev/null @@ -1,194 +0,0 @@ -using Microsoft.Extensions.AI; -using ModelContextProtocol.Protocol.Types; -using ModelContextProtocol.Server; -using Moq; - -namespace ModelContextProtocol.Tests.Server; -public class McpServerToolReturnTests -{ - [Fact] - public async Task CanReturnCollectionOfAIContent() - { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => - { - Assert.Same(mockServer.Object, server); - return new List() { - new TextContent("text"), - new DataContent("data:image/png;base64,1234"), - new DataContent("data:audio/wav;base64,1234") - }; - }); - - var result = await tool.InvokeAsync( - new RequestContext(mockServer.Object, null), - TestContext.Current.CancellationToken); - - Assert.Equal(3, 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/png", result.Content[1].MimeType); - Assert.Equal("image", result.Content[1].Type); - - Assert.Equal("1234", result.Content[2].Data); - Assert.Equal("audio/wav", result.Content[2].MimeType); - Assert.Equal("audio", result.Content[2].Type); - } - - [Theory] - [InlineData("text", "text")] - [InlineData("data:image/png;base64,1234", "image")] - [InlineData("data:audio/wav;base64,1234", "audio")] - public async Task CanReturnSingleAIContent(string data, string type) - { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => - { - Assert.Same(mockServer.Object, server); - return type switch - { - "text" => (AIContent)new TextContent(data), - "image" => new DataContent(data), - "audio" => new DataContent(data), - _ => throw new ArgumentException("Invalid type") - }; - }); - - var result = await tool.InvokeAsync( - new RequestContext(mockServer.Object, null), - TestContext.Current.CancellationToken); - - Assert.Single(result.Content); - Assert.Equal(type, result.Content[0].Type); - - if (type != "text") - { - Assert.NotNull(result.Content[0].MimeType); - Assert.Equal(data.Split(',').Last(), result.Content[0].Data); - } - else - { - Assert.Null(result.Content[0].MimeType); - Assert.Equal(data, result.Content[0].Text); - } - } - - [Fact] - public async Task CanReturnNullAIContent() - { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => - { - Assert.Same(mockServer.Object, server); - return (string?)null; - }); - var result = await tool.InvokeAsync( - new RequestContext(mockServer.Object, null), - TestContext.Current.CancellationToken); - Assert.Empty(result.Content); - } - - [Fact] - public async Task CanReturnString() - { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => - { - Assert.Same(mockServer.Object, server); - return "42"; - }); - var result = await tool.InvokeAsync( - new RequestContext(mockServer.Object, null), - TestContext.Current.CancellationToken); - Assert.Single(result.Content); - Assert.Equal("42", result.Content[0].Text); - Assert.Equal("text", result.Content[0].Type); - } - - [Fact] - public async Task CanReturnCollectionOfStrings() - { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => - { - Assert.Same(mockServer.Object, server); - return new List() { "42", "43" }; - }); - var result = await tool.InvokeAsync( - new RequestContext(mockServer.Object, null), - 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); - } - - [Fact] - public async Task CanReturnMcpContent() - { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => - { - Assert.Same(mockServer.Object, server); - return new Content { Text = "42", Type = "text" }; - }); - var result = await tool.InvokeAsync( - new RequestContext(mockServer.Object, null), - TestContext.Current.CancellationToken); - Assert.Single(result.Content); - Assert.Equal("42", result.Content[0].Text); - Assert.Equal("text", result.Content[0].Type); - } - - [Fact] - public async Task CanReturnCollectionOfMcpContent() - { - Mock mockServer = new(); - 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" } }; - }); - var result = await tool.InvokeAsync( - new RequestContext(mockServer.Object, null), - 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); - } - - [Fact] - public async Task CanReturnCallToolResponse() - { - CallToolResponse response = new() - { - Content = [new() { Text = "text", Type = "text" }, new() { Data = "1234", Type = "image" }] - }; - - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => - { - Assert.Same(mockServer.Object, server); - return response; - }); - var result = await tool.InvokeAsync( - new RequestContext(mockServer.Object, null), - TestContext.Current.CancellationToken); - - 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); - } -} diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs index 3f066dd5c..c526231ab 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs @@ -13,8 +13,10 @@ public class McpServerToolTests [Fact] public void Create_InvalidArgs_Throws() { - Assert.Throws("function", () => McpServerTool.Create(null!)); + Assert.Throws("function", () => McpServerTool.Create((AIFunction)null!)); Assert.Throws("method", () => McpServerTool.Create((MethodInfo)null!)); + Assert.Throws("method", () => McpServerTool.Create((MethodInfo)null!, typeof(object))); + Assert.Throws("targetType", () => McpServerTool.Create(typeof(McpServerToolTests).GetMethod(nameof(Create_InvalidArgs_Throws))!, (Type)null!)); Assert.Throws("method", () => McpServerTool.Create((Delegate)null!)); } @@ -50,7 +52,7 @@ public async Task SupportsServiceFromDI() { Assert.Same(expectedMyService, actualMyService); return "42"; - }, services: services); + }, new() { Services = services }); Assert.DoesNotContain("actualMyService", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema)); @@ -82,7 +84,7 @@ public async Task SupportsOptionalServiceFromDI() { Assert.Null(actualMyService); return "42"; - }, services: services); + }, new() { Services = services }); var result = await tool.InvokeAsync( new RequestContext(null!, null), @@ -129,6 +131,193 @@ public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposable Assert.Equal("""{"asyncDisposals":1,"disposals":0}""", result.Content[0].Text); } + + [Fact] + public async Task CanReturnCollectionOfAIContent() + { + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((IMcpServer server) => + { + Assert.Same(mockServer.Object, server); + return new List() { + new TextContent("text"), + new DataContent("data:image/png;base64,1234"), + new DataContent("data:audio/wav;base64,1234") + }; + }); + + var result = await tool.InvokeAsync( + new RequestContext(mockServer.Object, null), + TestContext.Current.CancellationToken); + + Assert.Equal(3, 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/png", result.Content[1].MimeType); + Assert.Equal("image", result.Content[1].Type); + + Assert.Equal("1234", result.Content[2].Data); + Assert.Equal("audio/wav", result.Content[2].MimeType); + Assert.Equal("audio", result.Content[2].Type); + } + + [Theory] + [InlineData("text", "text")] + [InlineData("data:image/png;base64,1234", "image")] + [InlineData("data:audio/wav;base64,1234", "audio")] + public async Task CanReturnSingleAIContent(string data, string type) + { + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((IMcpServer server) => + { + Assert.Same(mockServer.Object, server); + return type switch + { + "text" => (AIContent)new TextContent(data), + "image" => new DataContent(data), + "audio" => new DataContent(data), + _ => throw new ArgumentException("Invalid type") + }; + }); + + var result = await tool.InvokeAsync( + new RequestContext(mockServer.Object, null), + TestContext.Current.CancellationToken); + + Assert.Single(result.Content); + Assert.Equal(type, result.Content[0].Type); + + if (type != "text") + { + Assert.NotNull(result.Content[0].MimeType); + Assert.Equal(data.Split(',').Last(), result.Content[0].Data); + } + else + { + Assert.Null(result.Content[0].MimeType); + Assert.Equal(data, result.Content[0].Text); + } + } + + [Fact] + public async Task CanReturnNullAIContent() + { + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((IMcpServer server) => + { + Assert.Same(mockServer.Object, server); + return (string?)null; + }); + var result = await tool.InvokeAsync( + new RequestContext(mockServer.Object, null), + TestContext.Current.CancellationToken); + Assert.Empty(result.Content); + } + + [Fact] + public async Task CanReturnString() + { + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((IMcpServer server) => + { + Assert.Same(mockServer.Object, server); + return "42"; + }); + var result = await tool.InvokeAsync( + new RequestContext(mockServer.Object, null), + TestContext.Current.CancellationToken); + Assert.Single(result.Content); + Assert.Equal("42", result.Content[0].Text); + Assert.Equal("text", result.Content[0].Type); + } + + [Fact] + public async Task CanReturnCollectionOfStrings() + { + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((IMcpServer server) => + { + Assert.Same(mockServer.Object, server); + return new List() { "42", "43" }; + }); + var result = await tool.InvokeAsync( + new RequestContext(mockServer.Object, null), + 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); + } + + [Fact] + public async Task CanReturnMcpContent() + { + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((IMcpServer server) => + { + Assert.Same(mockServer.Object, server); + return new Content { Text = "42", Type = "text" }; + }); + var result = await tool.InvokeAsync( + new RequestContext(mockServer.Object, null), + TestContext.Current.CancellationToken); + Assert.Single(result.Content); + Assert.Equal("42", result.Content[0].Text); + Assert.Equal("text", result.Content[0].Type); + } + + [Fact] + public async Task CanReturnCollectionOfMcpContent() + { + Mock mockServer = new(); + 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" } }; + }); + var result = await tool.InvokeAsync( + new RequestContext(mockServer.Object, null), + 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); + } + + [Fact] + public async Task CanReturnCallToolResponse() + { + CallToolResponse response = new() + { + Content = [new() { Text = "text", Type = "text" }, new() { Data = "1234", Type = "image" }] + }; + + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((IMcpServer server) => + { + Assert.Same(mockServer.Object, server); + return response; + }); + var result = await tool.InvokeAsync( + new RequestContext(mockServer.Object, null), + TestContext.Current.CancellationToken); + + 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); + } + private sealed class MyService; private class DisposableToolType : IDisposable From 23bb93f51c23d788bce07394fa60d4e14e59ed27 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 28 Mar 2025 10:51:41 -0400 Subject: [PATCH 2/2] Address feedback --- .../Server/AIFunctionMcpServerTool.cs | 16 ++--- .../Server/McpServerToolAttribute.cs | 59 ++++++++++--------- .../Server/McpServerToolTests.cs | 3 +- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs index 353aa92e6..f65837710 100644 --- a/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs @@ -185,24 +185,24 @@ options.OpenWorld is not null || newOptions.Name ??= attr.Name; newOptions.Title ??= attr.Title; - if (attr.DestructiveIsSet) + if (attr._destructive is bool destructive) { - newOptions.Destructive ??= attr.Destructive; + newOptions.Destructive ??= destructive; } - if (attr.IdempotentIsSet) + if (attr._idempotent is bool idempotent) { - newOptions.Idempotent ??= attr.Idempotent; + newOptions.Idempotent ??= idempotent; } - if (attr.OpenWorldIsSet) + if (attr._openWorld is bool openWorld) { - newOptions.OpenWorld ??= attr.OpenWorld; + newOptions.OpenWorld ??= openWorld; } - if (attr.ReadOnlyIsSet) + if (attr._readOnly is bool readOnly) { - newOptions.ReadOnly ??= attr.ReadOnly; + newOptions.ReadOnly ??= readOnly; } } diff --git a/src/ModelContextProtocol/Server/McpServerToolAttribute.cs b/src/ModelContextProtocol/Server/McpServerToolAttribute.cs index 9ae52aa7d..5320fafce 100644 --- a/src/ModelContextProtocol/Server/McpServerToolAttribute.cs +++ b/src/ModelContextProtocol/Server/McpServerToolAttribute.cs @@ -6,10 +6,17 @@ [AttributeUsage(AttributeTargets.Method)] public sealed class McpServerToolAttribute : Attribute { - private bool? _destructive; - private bool? _idempotent; - private bool? _openWorld; - private bool? _readOnly; + // Defaults based on the spec + private const bool DestructiveDefault = true; + private const bool IdempotentDefault = false; + private const bool OpenWorldDefault = true; + private const bool ReadOnlyDefault = false; + + // Nullable backing fields so we can distinguish + internal bool? _destructive; + internal bool? _idempotent; + internal bool? _openWorld; + internal bool? _readOnly; /// /// Initializes a new instance of the class. @@ -30,41 +37,37 @@ public McpServerToolAttribute() /// /// Gets or sets whether the tool may perform destructive updates to its environment. /// - public bool Destructive { get => _destructive ?? true; set => _destructive = value; } - - /// - /// Gets whether the property has been assigned a value. - /// - public bool DestructiveIsSet => _destructive.HasValue; + public bool Destructive + { + get => _destructive ?? DestructiveDefault; + set => _destructive = value; + } /// /// Gets or sets whether calling the tool repeatedly with the same arguments will have no additional effect on its environment. /// - public bool Idempotent { get => _idempotent ?? false; set => _idempotent = value; } - - /// - /// Gets whether the property has been assigned a value. - /// - public bool IdempotentIsSet => _idempotent.HasValue; + public bool Idempotent + { + get => _idempotent ?? IdempotentDefault; + set => _idempotent = value; + } /// /// Gets or sets whether this tool may interact with an "open world" of external entities /// (e.g. the world of a web search tool is open, whereas that of a memory tool is not). /// - public bool OpenWorld { get => _openWorld ?? true; set => _openWorld = value; } - - /// - /// Gets whether the property has been assigned a value. - /// - public bool OpenWorldIsSet => _openWorld.HasValue; + public bool OpenWorld + { + get => _openWorld ?? OpenWorldDefault; + set => _openWorld = value; + } /// /// Gets or sets whether the tool does not modify its environment. /// - public bool ReadOnly { get => _readOnly ?? false; set => _readOnly = value; } - - /// - /// Gets whether the property has been assigned a value. - /// - public bool ReadOnlyIsSet => _readOnly.HasValue; + public bool ReadOnly + { + get => _readOnly ?? ReadOnlyDefault; + set => _readOnly = value; + } } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs index c526231ab..5c04caadd 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Server; using Moq;