diff --git a/Directory.Packages.props b/Directory.Packages.props index 4c36120..dbdf26d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,14 +2,14 @@ true 10.0.0-preview.2.25163.2 - 9.3.0-preview.1.25161.3 + 9.4.0-preview.1.25207.5 - + - + @@ -18,15 +18,15 @@ - + - - - + + + @@ -49,11 +49,11 @@ - - - - - + + + + + diff --git a/samples/ChatWithTools/Program.cs b/samples/ChatWithTools/Program.cs index 355a802..dd09a7c 100644 --- a/samples/ChatWithTools/Program.cs +++ b/samples/ChatWithTools/Program.cs @@ -25,7 +25,7 @@ Console.WriteLine(); // Create an IChatClient. (This shows using OpenAIClient, but it could be any other IChatClient implementation.) // Provide your own OPENAI_API_KEY via an environment variable. using IChatClient chatClient = - new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")).AsChatClient("gpt-4o-mini") + new OpenAIClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")).GetChatClient("gpt-4o-mini").AsIChatClient() .AsBuilder().UseFunctionInvocation().Build(); // Have a conversation, making all tools available to the LLM. diff --git a/src/ModelContextProtocol/Client/McpClientTool.cs b/src/ModelContextProtocol/Client/McpClientTool.cs index 9dfee5a..5d3364c 100644 --- a/src/ModelContextProtocol/Client/McpClientTool.cs +++ b/src/ModelContextProtocol/Client/McpClientTool.cs @@ -2,12 +2,20 @@ using ModelContextProtocol.Utils.Json; using Microsoft.Extensions.AI; using System.Text.Json; +using System.Collections.ObjectModel; namespace ModelContextProtocol.Client; /// Provides an AI function that calls a tool through . public sealed class McpClientTool : AIFunction { + /// Additional properties exposed from tools. + private static readonly ReadOnlyDictionary s_additionalProperties = + new(new Dictionary() + { + ["Strict"] = false, // some MCP schemas may not meet "strict" requirements + }); + private readonly IMcpClient _client; private readonly string _name; private readonly string _description; @@ -62,14 +70,13 @@ public sealed class McpClientTool : AIFunction public override JsonSerializerOptions JsonSerializerOptions { get; } /// - protected async override Task InvokeCoreAsync( - IEnumerable> arguments, CancellationToken cancellationToken) - { - IReadOnlyDictionary argDict = - arguments as IReadOnlyDictionary ?? - arguments.ToDictionary(); + public override IReadOnlyDictionary AdditionalProperties => s_additionalProperties; - CallToolResponse result = await _client.CallToolAsync(ProtocolTool.Name, argDict, JsonSerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + /// + protected async override ValueTask InvokeCoreAsync( + AIFunctionArguments arguments, CancellationToken cancellationToken) + { + CallToolResponse result = await _client.CallToolAsync(ProtocolTool.Name, arguments, JsonSerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse); } } \ No newline at end of file diff --git a/src/ModelContextProtocol/ModelContextProtocol.csproj b/src/ModelContextProtocol/ModelContextProtocol.csproj index e56aa73..e818426 100644 --- a/src/ModelContextProtocol/ModelContextProtocol.csproj +++ b/src/ModelContextProtocol/ModelContextProtocol.csproj @@ -30,14 +30,10 @@ + - - diff --git a/src/ModelContextProtocol/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol/Server/AIFunctionMcpServerPrompt.cs index 3e560db..9d84184 100644 --- a/src/ModelContextProtocol/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol/Server/AIFunctionMcpServerPrompt.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Utils; -using ModelContextProtocol.Utils.Json; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; @@ -12,10 +11,6 @@ namespace ModelContextProtocol.Server; /// Provides an that's implemented via an . internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt { - /// Key used temporarily for flowing request context into an AIFunction. - /// This will be replaced with use of AIFunctionArguments.Context. - internal const string RequestContextKey = "__temporary_RequestContext"; - /// /// Creates an instance for a method, specified via a instance. /// @@ -40,17 +35,10 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt { Throw.IfNull(method); - // TODO: Once this repo consumes a new build of Microsoft.Extensions.AI containing - // https://github.com/dotnet/extensions/pull/6158, - // https://github.com/dotnet/extensions/pull/6162, and - // https://github.com/dotnet/extensions/pull/6175, switch over to using the real - // AIFunctionFactory, delete the TemporaryXx types, and fix-up the mechanism by - // which the arguments are passed. - options = DeriveOptions(method, options); return Create( - TemporaryAIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, options)), + AIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, options)), options); } @@ -67,17 +55,17 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt options = DeriveOptions(method, options); return Create( - TemporaryAIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, options)), + AIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, options)), options); } - private static TemporaryAIFunctionFactoryOptions CreateAIFunctionFactoryOptions( + private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( MethodInfo method, McpServerPromptCreateOptions? options) => new() { Name = options?.Name ?? method.GetCustomAttribute()?.Name, Description = options?.Description, - MarshalResult = static (result, _, cancellationToken) => Task.FromResult(result), + MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), ConfigureParameterBinding = pi => { if (pi.ParameterType == typeof(RequestContext)) @@ -129,9 +117,9 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt return default; - static RequestContext? GetRequestContext(IReadOnlyDictionary args) + static RequestContext? GetRequestContext(AIFunctionArguments args) { - if (args.TryGetValue(RequestContextKey, out var orc) && + if (args.Context?.TryGetValue(typeof(RequestContext), out var orc) is true && orc is RequestContext requestContext) { return requestContext; @@ -204,14 +192,22 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt RequestContext request, CancellationToken cancellationToken = default) { Throw.IfNull(request); - cancellationToken.ThrowIfCancellationRequested(); - // TODO: Once we shift to the real AIFunctionFactory, the request should be passed via AIFunctionArguments.Context. - Dictionary arguments = request.Params?.Arguments is { } paramArgs ? - paramArgs.ToDictionary(entry => entry.Key, entry => entry.Value.AsObject()) : - []; - arguments[RequestContextKey] = request; + AIFunctionArguments arguments = new() + { + Services = request.Server?.Services, + Context = new Dictionary() { [typeof(RequestContext)] = request } + }; + + var argDict = request.Params?.Arguments; + if (argDict is not null) + { + foreach (var kvp in argDict) + { + arguments[kvp.Key] = kvp.Value; + } + } object? result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false); diff --git a/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs index ad37f37..5e4b1e1 100644 --- a/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs @@ -12,10 +12,6 @@ namespace ModelContextProtocol.Server; /// Provides an that's implemented via an . internal sealed class AIFunctionMcpServerTool : McpServerTool { - /// Key used temporarily for flowing request context into an AIFunction. - /// This will be replaced with use of AIFunctionArguments.Context. - internal const string RequestContextKey = "__temporary_RequestContext"; - /// /// Creates an instance for a method, specified via a instance. /// @@ -40,17 +36,10 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool { Throw.IfNull(method); - // TODO: Once this repo consumes a new build of Microsoft.Extensions.AI containing - // https://github.com/dotnet/extensions/pull/6158, - // https://github.com/dotnet/extensions/pull/6162, and - // https://github.com/dotnet/extensions/pull/6175, switch over to using the real - // AIFunctionFactory, delete the TemporaryXx types, and fix-up the mechanism by - // which the arguments are passed. - options = DeriveOptions(method, options); return Create( - TemporaryAIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, options)), + AIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, options)), options); } @@ -67,17 +56,17 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool options = DeriveOptions(method, options); return Create( - TemporaryAIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, options)), + AIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, options)), options); } - private static TemporaryAIFunctionFactoryOptions CreateAIFunctionFactoryOptions( + private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( MethodInfo method, McpServerToolCreateOptions? options) => new() { Name = options?.Name ?? method.GetCustomAttribute()?.Name, Description = options?.Description, - MarshalResult = static (result, _, cancellationToken) => Task.FromResult(result), + MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), ConfigureParameterBinding = pi => { if (pi.ParameterType == typeof(RequestContext)) @@ -150,9 +139,9 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool return default; - static RequestContext? GetRequestContext(IReadOnlyDictionary args) + static RequestContext? GetRequestContext(AIFunctionArguments args) { - if (args.TryGetValue(RequestContextKey, out var orc) && + if (args.Context?.TryGetValue(typeof(RequestContext), out var orc) is true && orc is RequestContext requestContext) { return requestContext; @@ -251,14 +240,22 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool RequestContext request, CancellationToken cancellationToken = default) { Throw.IfNull(request); - cancellationToken.ThrowIfCancellationRequested(); - // TODO: Once we shift to the real AIFunctionFactory, the request should be passed via AIFunctionArguments.Context. - Dictionary arguments = request.Params?.Arguments is { } paramArgs ? - paramArgs.ToDictionary(entry => entry.Key, entry => entry.Value.AsObject()) : - []; - arguments[RequestContextKey] = request; + AIFunctionArguments arguments = new() + { + Services = request.Server?.Services, + Context = new Dictionary() { [typeof(RequestContext)] = request } + }; + + var argDict = request.Params?.Arguments; + if (argDict is not null) + { + foreach (var kvp in argDict) + { + arguments[kvp.Key] = kvp.Value; + } + } object? result; try @@ -313,8 +310,6 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool CallToolResponse callToolResponse => callToolResponse, - // TODO https://github.com/modelcontextprotocol/csharp-sdk/issues/69: - // Add specialization for annotations. _ => new() { Content = [new() diff --git a/src/ModelContextProtocol/Server/TemporaryAIFunctionFactory.Utilities.cs b/src/ModelContextProtocol/Server/TemporaryAIFunctionFactory.Utilities.cs deleted file mode 100644 index d0596f8..0000000 --- a/src/ModelContextProtocol/Server/TemporaryAIFunctionFactory.Utilities.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using ModelContextProtocol.Utils; -using System.Buffers; -using System.Reflection; -using System.Text.RegularExpressions; - -namespace Microsoft.Extensions.AI; - -internal static partial class TemporaryAIFunctionFactory -{ - /// - /// Removes characters from a .NET member name that shouldn't be used in an AI function name. - /// - /// The .NET member name that should be sanitized. - /// - /// Replaces non-alphanumeric characters in the identifier with the underscore character. - /// Primarily intended to remove characters produced by compiler-generated method name mangling. - /// - internal static string SanitizeMemberName(string memberName) - { - Throw.IfNull(memberName); - return InvalidNameCharsRegex().Replace(memberName, "_"); - } - - /// Regex that flags any character other than ASCII digits or letters or the underscore. -#if NET - [GeneratedRegex("[^0-9A-Za-z_]")] - private static partial Regex InvalidNameCharsRegex(); -#else - private static Regex InvalidNameCharsRegex() => _invalidNameCharsRegex; - private static readonly Regex _invalidNameCharsRegex = new("[^0-9A-Za-z_]", RegexOptions.Compiled); -#endif - - /// Invokes the MethodInfo with the specified target object and arguments. - private static object? ReflectionInvoke(MethodInfo method, object? target, object?[]? arguments) - { -#if NET - return method.Invoke(target, BindingFlags.DoNotWrapExceptions, binder: null, arguments, culture: null); -#else - try - { - return method.Invoke(target, BindingFlags.Default, binder: null, arguments, culture: null); - } - catch (TargetInvocationException e) when (e.InnerException is not null) - { - // If we're targeting .NET Framework, such that BindingFlags.DoNotWrapExceptions - // is ignored, the original exception will be wrapped in a TargetInvocationException. - // Unwrap it and throw that original exception, maintaining its stack information. - System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e.InnerException).Throw(); - throw; - } -#endif - } - - /// - /// Implements a simple write-only memory stream that uses pooled buffers. - /// - private sealed class PooledMemoryStream : Stream - { - private const int DefaultBufferSize = 4096; - private byte[] _buffer; - private int _position; - - public PooledMemoryStream(int initialCapacity = DefaultBufferSize) - { - _buffer = ArrayPool.Shared.Rent(initialCapacity); - _position = 0; - } - - public ReadOnlySpan GetBuffer() => _buffer.AsSpan(0, _position); - public override bool CanWrite => true; - public override bool CanRead => false; - public override bool CanSeek => false; - public override long Length => _position; - public override long Position - { - get => _position; - set => throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - EnsureNotDisposed(); - EnsureCapacity(_position + count); - - Buffer.BlockCopy(buffer, offset, _buffer, _position, count); - _position += count; - } - - public override void Flush() - { - } - - public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - public override void SetLength(long value) => throw new NotSupportedException(); - - protected override void Dispose(bool disposing) - { - if (_buffer is not null) - { - ArrayPool.Shared.Return(_buffer); - _buffer = null!; - } - - base.Dispose(disposing); - } - - private void EnsureCapacity(int requiredCapacity) - { - if (requiredCapacity <= _buffer.Length) - { - return; - } - - int newCapacity = Math.Max(requiredCapacity, _buffer.Length * 2); - byte[] newBuffer = ArrayPool.Shared.Rent(newCapacity); - Buffer.BlockCopy(_buffer, 0, newBuffer, 0, _position); - - ArrayPool.Shared.Return(_buffer); - _buffer = newBuffer; - } - - private void EnsureNotDisposed() - { - if (_buffer is null) - { - Throw(); - static void Throw() => throw new ObjectDisposedException(nameof(PooledMemoryStream)); - } - } - } -} diff --git a/src/ModelContextProtocol/Server/TemporaryAIFunctionFactory.cs b/src/ModelContextProtocol/Server/TemporaryAIFunctionFactory.cs deleted file mode 100644 index 17a0981..0000000 --- a/src/ModelContextProtocol/Server/TemporaryAIFunctionFactory.cs +++ /dev/null @@ -1,791 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.DependencyInjection; -using ModelContextProtocol.Protocol.Types; -using ModelContextProtocol.Server; -using ModelContextProtocol.Utils; -using ModelContextProtocol.Utils.Json; -using System.Collections.Concurrent; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - -#if !NET -using System.Linq; -#endif -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization.Metadata; -using static ModelContextProtocol.Utils.Json.McpJsonUtilities; - -#pragma warning disable CA1031 // Do not catch general exception types -#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields -#pragma warning disable SA1118 // Parameter should not span multiple lines -#pragma warning disable SA1500 // Braces for multi-line statements should not share line - -namespace Microsoft.Extensions.AI; - -/// Provides factory methods for creating commonly used implementations of . -internal static partial class TemporaryAIFunctionFactory -{ - /// Holds the default options instance used when creating function. - private static readonly TemporaryAIFunctionFactoryOptions _defaultOptions = new(); - - /// Creates an instance for a method, specified via a delegate. - /// The method to be represented via the created . - /// Metadata to use to override defaults inferred from . - /// The created for invoking . - /// - /// - /// Return values are serialized to using 's - /// . Arguments that are not already of the expected type are - /// marshaled to the expected type via JSON and using 's - /// . If the argument is a , - /// , or , it is deserialized directly. If the argument is anything else unknown, - /// it is round-tripped through JSON, serializing the object as JSON and then deserializing it to the expected type. - /// - /// - /// is . - public static AIFunction Create(Delegate method, TemporaryAIFunctionFactoryOptions? options) - { - Throw.IfNull(method); - - return ReflectionAIFunction.Build(method.Method, method.Target, options ?? _defaultOptions); - } - - /// Creates an instance for a method, specified via a delegate. - /// The method to be represented via the created . - /// The name to use for the . - /// The description to use for the . - /// The used to marshal function parameters and any return value. - /// The created for invoking . - /// - /// - /// Return values are serialized to using . - /// Arguments that are not already of the expected type are marshaled to the expected type via JSON and using - /// . If the argument is a , , - /// or , it is deserialized directly. If the argument is anything else unknown, it is - /// round-tripped through JSON, serializing the object as JSON and then deserializing it to the expected type. - /// - /// - /// is . - public static AIFunction Create(Delegate method, string? name = null, string? description = null, JsonSerializerOptions? serializerOptions = null) - { - Throw.IfNull(method); - - TemporaryAIFunctionFactoryOptions createOptions = serializerOptions is null && name is null && description is null - ? _defaultOptions - : new() - { - Name = name, - Description = description, - SerializerOptions = serializerOptions, - }; - - return ReflectionAIFunction.Build(method.Method, method.Target, createOptions); - } - - /// - /// Creates an instance for a method, specified via an instance - /// and an optional target object if the method is an instance method. - /// - /// The method to be represented via the created . - /// - /// The target object for the if it represents an instance method. - /// This should be if and only if is a static method. - /// - /// Metadata to use to override defaults inferred from . - /// The created for invoking . - /// - /// - /// Return values are serialized to using 's - /// . Arguments that are not already of the expected type are - /// marshaled to the expected type via JSON and using 's - /// . If the argument is a , - /// , or , it is deserialized directly. If the argument is anything else unknown, - /// it is round-tripped through JSON, serializing the object as JSON and then deserializing it to the expected type. - /// - /// - /// is . - public static AIFunction Create(MethodInfo method, object? target, TemporaryAIFunctionFactoryOptions? options) - { - Throw.IfNull(method); - return ReflectionAIFunction.Build(method, target, options ?? _defaultOptions); - } - - /// - /// Creates an instance for a method, specified via an instance - /// and an optional target object if the method is an instance method. - /// - /// The instance method to be represented via the created . - /// - /// The to construct an instance of on which to invoke when - /// the resulting is invoked. If services are provided, - /// ActivatorUtilities.CreateInstance will be used to construct the instance using those services; otherwise, - /// 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. - /// - /// Metadata to use to override defaults inferred from . - /// The created for invoking . - /// - /// - /// Return values are serialized to using 's - /// . Arguments that are not already of the expected type are - /// marshaled to the expected type via JSON and using 's - /// . If the argument is a , - /// , or , it is deserialized directly. If the argument is anything else unknown, - /// it is round-tripped through JSON, serializing the object as JSON and then deserializing it to the expected type. - /// - /// - /// is . - public static AIFunction Create( - MethodInfo method, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, - TemporaryAIFunctionFactoryOptions? options = null) - { - Throw.IfNull(method); - Throw.IfNull(targetType); - - return ReflectionAIFunction.Build(method, targetType, options ?? _defaultOptions); - } - - /// - /// Creates an instance for a method, specified via an instance - /// and an optional target object if the method is an instance method. - /// - /// The method to be represented via the created . - /// - /// The target object for the if it represents an instance method. - /// This should be if and only if is a static method. - /// - /// The name to use for the . - /// The description to use for the . - /// The used to marshal function parameters and return value. - /// The created for invoking . - /// - /// - /// Return values are serialized to using . - /// Arguments that are not already of the expected type are marshaled to the expected type via JSON and using - /// . If the argument is a , , - /// or , it is deserialized directly. If the argument is anything else unknown, it is - /// round-tripped through JSON, serializing the object as JSON and then deserializing it to the expected type. - /// - /// - /// is . - public static AIFunction Create(MethodInfo method, object? target, string? name = null, string? description = null, JsonSerializerOptions? serializerOptions = null) - { - Throw.IfNull(method); - - TemporaryAIFunctionFactoryOptions createOptions = serializerOptions is null && name is null && description is null - ? _defaultOptions - : new() - { - Name = name, - Description = description, - SerializerOptions = serializerOptions, - }; - - return ReflectionAIFunction.Build(method, target, createOptions); - } - - private sealed class ReflectionAIFunction : AIFunction - { - public static ReflectionAIFunction Build(MethodInfo method, object? target, TemporaryAIFunctionFactoryOptions options) - { - Throw.IfNull(method); - - if (method.ContainsGenericParameters) - { - throw new ArgumentException("Open generic methods are not supported", nameof(method)); - } - - if (method.IsStatic) - { - if (target is not null) - { - throw new ArgumentException("The specified method is a static method but the specified target is non-null.", nameof(target)); - } - } - else if (target is null) - { - throw new ArgumentNullException(nameof(target), "The specified method is an instance method but the specified target is null."); - } - - var functionDescriptor = ReflectionAIFunctionDescriptor.GetOrCreate(method, options); - - if (target is null && options.AdditionalProperties is null) - { - // We can use a cached value for static methods not specifying additional properties. - return functionDescriptor.CachedDefaultInstance ??= new(functionDescriptor, target, options); - } - - return new(functionDescriptor, target, options); - } - - public static ReflectionAIFunction Build( - MethodInfo method, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, - TemporaryAIFunctionFactoryOptions options) - { - Throw.IfNull(method); - - if (method.ContainsGenericParameters) - { - throw new ArgumentException("Open generic methods are not supported", nameof(method)); - } - - if (method.IsStatic) - { - throw new ArgumentException("The method must be an instance method.", nameof(method)); - } - - if (method.DeclaringType is { } declaringType && - !declaringType.IsAssignableFrom(targetType)) - { - throw new ArgumentException("The target type must be assignable to the method's declaring type.", nameof(targetType)); - } - - return new(ReflectionAIFunctionDescriptor.GetOrCreate(method, options), targetType, options); - } - - private ReflectionAIFunction(ReflectionAIFunctionDescriptor functionDescriptor, object? target, TemporaryAIFunctionFactoryOptions options) - { - FunctionDescriptor = functionDescriptor; - Target = target; - AdditionalProperties = options.AdditionalProperties ?? new Dictionary(); - } - - private ReflectionAIFunction( - ReflectionAIFunctionDescriptor functionDescriptor, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, - TemporaryAIFunctionFactoryOptions options) - { - FunctionDescriptor = functionDescriptor; - TargetType = targetType; - AdditionalProperties = options.AdditionalProperties ?? new Dictionary(); - } - - public ReflectionAIFunctionDescriptor FunctionDescriptor { get; } - public object? Target { get; } - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - public Type? TargetType { get; } - public override IReadOnlyDictionary AdditionalProperties { get; } - public override string Name => FunctionDescriptor.Name; - public override string Description => FunctionDescriptor.Description; - public override MethodInfo UnderlyingMethod => FunctionDescriptor.Method; - public override JsonElement JsonSchema => FunctionDescriptor.JsonSchema; - public override JsonSerializerOptions JsonSerializerOptions => FunctionDescriptor.JsonSerializerOptions; - - protected override async Task InvokeCoreAsync( - IEnumerable> arguments, - CancellationToken cancellationToken) - { - Dictionary argumentsDictionary = arguments.ToDictionary(); - - bool disposeTarget = false; - object? target = Target; - try - { - if (TargetType is { } targetType) - { - Debug.Assert(target is null, "Expected target to be null when we have a non-null target type"); - Debug.Assert(!FunctionDescriptor.Method.IsStatic, "Expected an instance method"); - - if (argumentsDictionary.TryGetValue(AIFunctionMcpServerTool.RequestContextKey, out object? value) && - value is RequestContext requestContext && - requestContext.Server?.Services is { } services) - { - target = ActivatorUtilities.CreateInstance(services, targetType!); - } - else - { - target = Activator.CreateInstance(targetType); - } - - disposeTarget = true; - } - var paramMarshallers = FunctionDescriptor.ParameterMarshallers; - object?[] args = paramMarshallers.Length != 0 ? new object?[paramMarshallers.Length] : []; - - for (int i = 0; i < args.Length; i++) - { - args[i] = paramMarshallers[i](argumentsDictionary, cancellationToken); - } - - return await FunctionDescriptor.ReturnParameterMarshaller( - ReflectionInvoke(FunctionDescriptor.Method, target, args), cancellationToken).ConfigureAwait(false); - } - finally - { - if (disposeTarget) - { - if (target is IAsyncDisposable ad) - { - await ad.DisposeAsync().ConfigureAwait(false); - } - else if (target is IDisposable d) - { - d.Dispose(); - } - } - } - } - } - - /// - /// A descriptor for a .NET method-backed AIFunction that precomputes its marshalling delegates and JSON schema. - /// - private sealed class ReflectionAIFunctionDescriptor - { - private const int InnerCacheSoftLimit = 512; - private static readonly ConditionalWeakTable> _descriptorCache = new(); - - /// A boxed . - private static readonly object? _boxedDefaultCancellationToken = default(CancellationToken); - - /// - /// Gets or creates a descriptors using the specified method and options. - /// - public static ReflectionAIFunctionDescriptor GetOrCreate(MethodInfo method, TemporaryAIFunctionFactoryOptions options) - { - JsonSerializerOptions serializerOptions = options.SerializerOptions ?? AIJsonUtilities.DefaultOptions; - AIJsonSchemaCreateOptions schemaOptions = options.JsonSchemaCreateOptions ?? AIJsonSchemaCreateOptions.Default; - serializerOptions.MakeReadOnly(); - ConcurrentDictionary innerCache = _descriptorCache.GetOrCreateValue(serializerOptions); - - DescriptorKey key = new(method, options.Name, options.Description, options.ConfigureParameterBinding, options.MarshalResult, schemaOptions); - if (innerCache.TryGetValue(key, out ReflectionAIFunctionDescriptor? descriptor)) - { - return descriptor; - } - - descriptor = new(key, serializerOptions); - return innerCache.Count < InnerCacheSoftLimit - ? innerCache.GetOrAdd(key, descriptor) - : descriptor; - } - - private ReflectionAIFunctionDescriptor(DescriptorKey key, JsonSerializerOptions serializerOptions) - { - ParameterInfo[] parameters = key.Method.GetParameters(); - - // Determine how each parameter should be bound. - Dictionary? boundParameters = null; - if (parameters.Length != 0 && key.GetBindParameterOptions is not null) - { - boundParameters = new(parameters.Length); - for (int i = 0; i < parameters.Length; i++) - { - boundParameters[parameters[i]] = key.GetBindParameterOptions(parameters[i]); - } - } - - // Get marshaling delegates for parameters. - ParameterMarshallers = parameters.Length > 0 ? new Func, CancellationToken, object?>[parameters.Length] : []; - for (int i = 0; i < parameters.Length; i++) - { - if (boundParameters?.TryGetValue(parameters[i], out TemporaryAIFunctionFactoryOptions.ParameterBindingOptions options) is not true) - { - options = default; - } - - ParameterMarshallers[i] = GetParameterMarshaller(serializerOptions, options, parameters[i]); - } - - // Get a marshaling delegate for the return value. - ReturnParameterMarshaller = GetReturnParameterMarshaller(key, serializerOptions); - - Method = key.Method; - Name = key.Name ?? GetFunctionName(key.Method); - Description = key.Description ?? key.Method.GetCustomAttribute(inherit: true)?.Description ?? string.Empty; - JsonSerializerOptions = serializerOptions; - JsonSchema = CreateFunctionJsonSchema( - key.Method, - Name, - Description, - serializerOptions, - parameterInfo => - { - // AIFunctionArguments and IServiceProvider parameters are always excluded from the schema. - if (parameterInfo.ParameterType == typeof(IServiceProvider)) - { - return false; - } - - // If the parameter is marked as excluded by GetBindParameterOptions, exclude it. - if (boundParameters?.TryGetValue(parameterInfo, out var options) is true && - options.ExcludeFromSchema) - { - return false; - } - - // Everything else is included. - return true; - }); - } - - /// - /// Determines a JSON schema for the provided method. - /// - /// The method from which to extract schema information. - /// The title keyword used by the method schema. - /// The description keyword used by the method schema. - /// The options used to extract the schema from the specified type. - /// Delegate controlling whether to include a parameter in the schema. - /// A JSON schema document encoded as a . - /// is . - private static JsonElement CreateFunctionJsonSchema( - MethodBase method, - string? title = null, - string? description = null, - JsonSerializerOptions? serializerOptions = null, - Func? includeParameter = null) - { - Throw.IfNull(method); - - serializerOptions ??= DefaultOptions; - title ??= method.Name; - description ??= method.GetCustomAttribute()?.Description; - - JsonObject parameterSchemas = new(); - JsonArray? requiredProperties = null; - foreach (ParameterInfo parameter in method.GetParameters()) - { - if (string.IsNullOrWhiteSpace(parameter.Name)) - { - throw new ArgumentException("Parameter is missing a name.", nameof(parameter)); - } - - if (parameter.ParameterType == typeof(CancellationToken)) - { - // CancellationToken is a special case that, by convention, we don't want to include in the schema. - // Invocations of methods that include a CancellationToken argument should also special-case CancellationToken - // to pass along what relevant token into the method's invocation. - continue; - } - - if (includeParameter?.Invoke(parameter) is false) - { - continue; - } - - JsonNode? parameterSchema = JsonSerializer.SerializeToNode(AIJsonUtilities.CreateJsonSchema( - type: parameter.ParameterType, - description: parameter.GetCustomAttribute(inherit: true)?.Description, - hasDefaultValue: parameter.HasDefaultValue, - defaultValue: parameter.HasDefaultValue ? parameter.DefaultValue : null, - serializerOptions), McpJsonUtilities.JsonContext.Default.JsonElement); - - parameterSchemas.Add(parameter.Name, parameterSchema); - if (!parameter.IsOptional) - { - (requiredProperties ??= []).Add((JsonNode)parameter.Name); - } - } - - JsonObject schema = new(); - - if (!string.IsNullOrWhiteSpace(title)) - { - schema["title"] = title; - } - - if (!string.IsNullOrWhiteSpace(description)) - { - schema["description"] = description; - } - - schema["type"] = "object"; // Method schemas always hardcode the type as "object". - schema["properties"] = parameterSchemas; - - if (requiredProperties is not null) - { - schema["required"] = requiredProperties; - } - - return JsonSerializer.SerializeToElement(schema, JsonContext.Default.JsonNode); - } - - public string Name { get; } - public string Description { get; } - public MethodInfo Method { get; } - public JsonSerializerOptions JsonSerializerOptions { get; } - public JsonElement JsonSchema { get; } - public Func, CancellationToken, object?>[] ParameterMarshallers { get; } - public Func> ReturnParameterMarshaller { get; } - public ReflectionAIFunction? CachedDefaultInstance { get; set; } - - private static string GetFunctionName(MethodInfo method) - { - // Get the function name to use. - string name = SanitizeMemberName(method.Name); - - const string AsyncSuffix = "Async"; - if (IsAsyncMethod(method) && - name.EndsWith(AsyncSuffix, StringComparison.Ordinal) && - name.Length > AsyncSuffix.Length) - { - name = name.Substring(0, name.Length - AsyncSuffix.Length); - } - - return name; - - static bool IsAsyncMethod(MethodInfo method) - { - Type t = method.ReturnType; - - if (t == typeof(Task) || t == typeof(ValueTask)) - { - return true; - } - - if (t.IsGenericType) - { - t = t.GetGenericTypeDefinition(); - if (t == typeof(Task<>) || t == typeof(ValueTask<>) || t == typeof(IAsyncEnumerable<>)) - { - return true; - } - } - - return false; - } - } - - /// - /// Gets a delegate for handling the marshaling of a parameter. - /// - private static Func, CancellationToken, object?> GetParameterMarshaller( - JsonSerializerOptions serializerOptions, - TemporaryAIFunctionFactoryOptions.ParameterBindingOptions bindingOptions, - ParameterInfo parameter) - { - if (string.IsNullOrWhiteSpace(parameter.Name)) - { - throw new ArgumentException("Parameter is missing a name.", nameof(parameter)); - } - - Type parameterType = parameter.ParameterType; - - // For CancellationToken parameters, we always bind to the token passed directly to InvokeAsync. - if (parameterType == typeof(CancellationToken)) - { - return static (_, cancellationToken) => - cancellationToken == default ? _boxedDefaultCancellationToken : // optimize common case of a default CT to avoid boxing - cancellationToken; - } - - // CancellationToken is the only parameter type that's handled exclusively by the implementation. - // Now that it's been processed, check to see if the parameter should be handled via BindParameter. - if (bindingOptions.BindParameter is { } bindParameter) - { - return (arguments, _) => bindParameter(parameter, arguments); - } - - // We're now into default handling of everything else. - - // For IServiceProvider parameters, we bind to the services passed directly to InvokeAsync via AIFunctionArguments. - if (parameterType == typeof(IServiceProvider)) - { - return (arguments, _) => - { - arguments.TryGetValue("__temporary_IServiceProvider", out object? objServices); - IServiceProvider? services = objServices as IServiceProvider; - if (services is null && !parameter.HasDefaultValue) - { - throw new ArgumentException($"An {nameof(IServiceProvider)} was not provided for the {parameter.Name} parameter.", nameof(arguments)); - } - - return services; - }; - } - - // For all other parameters, create a marshaller that tries to extract the value from the arguments dictionary. - // Resolve the contract used to marshal the value from JSON -- can throw if not supported or not found. - JsonTypeInfo typeInfo = serializerOptions.GetTypeInfo(parameterType); - return (arguments, _) => - { - // If the parameter has an argument specified in the dictionary, return that argument. - if (arguments.TryGetValue(parameter.Name, out object? value)) - { - return value switch - { - null => null, // Return as-is if null -- if the parameter is a struct this will be handled by MethodInfo.Invoke - _ when parameterType.IsInstanceOfType(value) => value, // Do nothing if value is assignable to parameter type - JsonElement element => JsonSerializer.Deserialize(element, typeInfo), - JsonDocument doc => JsonSerializer.Deserialize(doc, typeInfo), - JsonNode node => JsonSerializer.Deserialize(node, typeInfo), - _ => MarshallViaJsonRoundtrip(value), - }; - - object? MarshallViaJsonRoundtrip(object value) - { - try - { - string json = JsonSerializer.Serialize(value, serializerOptions.GetTypeInfo(value.GetType())); - return JsonSerializer.Deserialize(json, typeInfo); - } - catch - { - // Eat any exceptions and fall back to the original value to force a cast exception later on. - return value; - } - } - } - - // If the parameter is required and there's no argument specified for it, throw. - if (!parameter.HasDefaultValue) - { - throw new ArgumentException($"Missing required parameter '{parameter.Name}' for method '{parameter.Member.Name}'.", nameof(arguments)); - } - - // Otherwise, use the optional parameter's default value. - return parameter.DefaultValue; - }; - } - - /// - /// Gets a delegate for handling the result value of a method, converting it into the to return from the invocation. - /// - private static Func> GetReturnParameterMarshaller( - DescriptorKey key, JsonSerializerOptions serializerOptions) - { - Type returnType = key.Method.ReturnType; - JsonTypeInfo returnTypeInfo; - Func>? marshalResult = key.MarshalResult; - - // Void - if (returnType == typeof(void)) - { - if (marshalResult is not null) - { - return (result, cancellationToken) => marshalResult(null, null, cancellationToken); - } - - return static (_, _) => Task.FromResult((object?)null); - } - - // Task - if (returnType == typeof(Task)) - { - if (marshalResult is not null) - { - return async (result, cancellationToken) => - { - await ((Task)ThrowIfNullResult(result)).ConfigureAwait(false); - return await marshalResult(null, null, cancellationToken).ConfigureAwait(false); - }; - } - - return async static (result, _) => - { - await ((Task)ThrowIfNullResult(result)).ConfigureAwait(false); - return null; - }; - } - - // ValueTask - if (returnType == typeof(ValueTask)) - { - if (marshalResult is not null) - { - return async (result, cancellationToken) => - { - await ((ValueTask)ThrowIfNullResult(result)).ConfigureAwait(false); - return await marshalResult(null, null, cancellationToken).ConfigureAwait(false); - }; - } - - return async static (result, _) => - { - await ((ValueTask)ThrowIfNullResult(result)).ConfigureAwait(false); - return null; - }; - } - - if (returnType.IsGenericType) - { - // Task - if (returnType.GetGenericTypeDefinition() == typeof(Task<>)) - { - MethodInfo taskResultGetter = GetMethodFromGenericMethodDefinition(returnType, _taskGetResult); - returnTypeInfo = serializerOptions.GetTypeInfo(taskResultGetter.ReturnType); - return async (taskObj, cancellationToken) => - { - await ((Task)ThrowIfNullResult(taskObj)).ConfigureAwait(false); - object? result = ReflectionInvoke(taskResultGetter, taskObj, null); - return marshalResult is not null ? - await marshalResult(result, returnTypeInfo.Type, cancellationToken).ConfigureAwait(false) : - await SerializeResultAsync(result, returnTypeInfo, cancellationToken).ConfigureAwait(false); - }; - } - - // ValueTask - if (returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) - { - MethodInfo valueTaskAsTask = GetMethodFromGenericMethodDefinition(returnType, _valueTaskAsTask); - MethodInfo asTaskResultGetter = GetMethodFromGenericMethodDefinition(valueTaskAsTask.ReturnType, _taskGetResult); - returnTypeInfo = serializerOptions.GetTypeInfo(asTaskResultGetter.ReturnType); - return async (taskObj, cancellationToken) => - { - var task = (Task)ReflectionInvoke(valueTaskAsTask, ThrowIfNullResult(taskObj), null)!; - await task.ConfigureAwait(false); - object? result = ReflectionInvoke(asTaskResultGetter, task, null); - return marshalResult is not null ? - await marshalResult(result, returnTypeInfo.Type, cancellationToken).ConfigureAwait(false) : - await SerializeResultAsync(result, returnTypeInfo, cancellationToken).ConfigureAwait(false); - }; - } - } - - // For everything else, just serialize the result as-is. - returnTypeInfo = serializerOptions.GetTypeInfo(returnType); - return marshalResult is not null ? - (result, cancellationToken) => marshalResult(result, returnTypeInfo.Type, cancellationToken) : - (result, cancellationToken) => SerializeResultAsync(result, returnTypeInfo, cancellationToken); - - static async Task SerializeResultAsync(object? result, JsonTypeInfo returnTypeInfo, CancellationToken cancellationToken) - { - if (returnTypeInfo.Kind is JsonTypeInfoKind.None) - { - // Special-case trivial contracts to avoid the more expensive general-purpose serialization path. - return JsonSerializer.SerializeToElement(result, returnTypeInfo); - } - - // Serialize asynchronously to support potential IAsyncEnumerable responses. - using PooledMemoryStream stream = new(); - await JsonSerializer.SerializeAsync(stream, result, returnTypeInfo, cancellationToken).ConfigureAwait(false); - Utf8JsonReader reader = new(stream.GetBuffer()); - return JsonElement.ParseValue(ref reader); - } - - // Throws an exception if a result is found to be null unexpectedly - static object ThrowIfNullResult(object? result) => result ?? throw new InvalidOperationException("Function returned null unexpectedly."); - } - - private static readonly MethodInfo _taskGetResult = typeof(Task<>).GetProperty(nameof(Task.Result), BindingFlags.Instance | BindingFlags.Public)!.GetMethod!; - private static readonly MethodInfo _valueTaskAsTask = typeof(ValueTask<>).GetMethod(nameof(ValueTask.AsTask), BindingFlags.Instance | BindingFlags.Public)!; - - private static MethodInfo GetMethodFromGenericMethodDefinition(Type specializedType, MethodInfo genericMethodDefinition) - { - Debug.Assert(specializedType.IsGenericType && specializedType.GetGenericTypeDefinition() == genericMethodDefinition.DeclaringType, "generic member definition doesn't match type."); -#if NET - return (MethodInfo)specializedType.GetMemberWithSameMetadataDefinitionAs(genericMethodDefinition); -#else - const BindingFlags All = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; - return specializedType.GetMethods(All).First(m => m.MetadataToken == genericMethodDefinition.MetadataToken); -#endif - } - - private record struct DescriptorKey( - MethodInfo Method, - string? Name, - string? Description, - Func? GetBindParameterOptions, - Func>? MarshalResult, - AIJsonSchemaCreateOptions SchemaOptions); - } -} diff --git a/src/ModelContextProtocol/Server/TemporaryAIFunctionFactoryOptions.cs b/src/ModelContextProtocol/Server/TemporaryAIFunctionFactoryOptions.cs deleted file mode 100644 index 91403b3..0000000 --- a/src/ModelContextProtocol/Server/TemporaryAIFunctionFactoryOptions.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; -using System.Reflection; -using System.Text.Json; - -namespace Microsoft.Extensions.AI; - -/// -/// Represents options that can be provided when creating an from a method. -/// -internal sealed class TemporaryAIFunctionFactoryOptions -{ - /// - /// Initializes a new instance of the class. - /// - public TemporaryAIFunctionFactoryOptions() - { - } - - /// Gets or sets the used to marshal .NET values being passed to the underlying delegate. - /// - /// If no value has been specified, the instance will be used. - /// - public JsonSerializerOptions? SerializerOptions { get; set; } - - /// - /// Gets or sets the governing the generation of JSON schemas for the function. - /// - /// - /// If no value has been specified, the instance will be used. - /// - public AIJsonSchemaCreateOptions? JsonSchemaCreateOptions { get; set; } - - /// Gets or sets the name to use for the function. - /// - /// The name to use for the function. The default value is a name derived from the method represented by the passed or . - /// - public string? Name { get; set; } - - /// Gets or sets the description to use for the function. - /// - /// The description for the function. The default value is a description derived from the passed or , if possible - /// (for example, via a on the method). - /// - public string? Description { get; set; } - - /// - /// Gets or sets additional values to store on the resulting property. - /// - /// - /// This property can be used to provide arbitrary information about the function. - /// - public IReadOnlyDictionary? AdditionalProperties { get; set; } - - /// Gets or sets a delegate used to determine how a particular parameter to the function should be bound. - /// - /// - /// If , the default parameter binding logic will be used. If non- value, - /// this delegate will be invoked once for each parameter in the function as part of creating the instance. - /// It is not invoked for parameters of type , which are invariably bound to the token - /// provided to the invocation. - /// - /// - /// Returning a default results in the same behavior as if - /// is . - /// - /// - public Func? ConfigureParameterBinding { get; set; } - - /// Gets or sets a delegate used to determine the returned by . - /// - /// - /// By default, the return value of invoking the method wrapped into an by - /// is then JSON serialized, with the resulting returned from the method. - /// This default behavior is ideal for the common case where the result will be passed back to an AI service. However, if the caller - /// requires more control over the result's marshaling, the property may be set to a delegate that is - /// then provided with complete control over the result's marshaling. The delegate is invoked with the value returned by the method, - /// and its return value is then returned from the method. - /// - /// - /// When set, the delegate is invoked even for -returning methods, in which case it is invoked with - /// a argument. By default, is returned from the - /// method for instances produced by to wrap - /// -returning methods). - /// - /// - /// Methods strongly-typed to return types of , , , - /// and are special-cased. For methods typed to return or , - /// will be invoked with the value after the returned task has successfully completed. - /// For methods typed to return or , the delegate will be invoked with the - /// task's result value after the task has successfully completed.These behaviors keep synchronous and asynchronous methods consistent. - /// - /// - /// In addition to the returned value, which is provided to the delegate as the first argument, the delegate is also provided with - /// a represented the declared return type of the method. This can be used to determine how to marshal the result. - /// This may be different than the actual type of the object () if the method returns a derived type - /// or . If the method is typed to return , , or , - /// the argument will be . - /// - /// - public Func>? MarshalResult { get; set; } - - /// Provides configuration options produced by the delegate. - public readonly record struct ParameterBindingOptions - { - /// Gets a delegate used to determine the value for a bound parameter. - /// - /// - /// The default value is . - /// - /// - /// If , the default binding semantics are used for the parameter. - /// If non- , each time the is invoked, this delegate will be invoked - /// to select the argument value to use for the parameter. The return value of the delegate will be used for the parameter's - /// value. - /// - /// - public Func, object?>? BindParameter { get; init; } - - /// Gets a value indicating whether the parameter should be excluded from the generated schema. - /// - /// - /// The default value is . - /// - /// - /// Typically, this property is set to if and only if is also set to - /// non-. While it's possible to exclude the schema when is , - /// doing so means that default marshaling will be used but the AI service won't be aware of the parameter or able to generate - /// an argument for it. This is likely to result in invocation errors, as the parameter information is unlikely to be available. - /// It, however, is permissible for cases where invocation of the is tightly controlled, and the caller - /// is expected to augment the argument dictionary with the parameter value. - /// - /// - public bool ExcludeFromSchema { get; init; } - } -} diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs index ec4dd04..58864a1 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs @@ -213,7 +213,7 @@ public class McpClientExtensionsTests : ClientServerTestBase var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); 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); + var result = await echo.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken); Assert.Contains("Method4 Result 42", result?.ToString()); var valuesSetViaAttr = tools.Single(t => t.Name == "ValuesSetViaAttr"); @@ -240,7 +240,7 @@ public class McpClientExtensionsTests : ClientServerTestBase { if (tool.Name == "Method4") { - var result = await tool.InvokeAsync(new Dictionary() { ["i"] = 42 }, TestContext.Current.CancellationToken); + var result = await tool.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken); Assert.Contains("Method4 Result 42", result?.ToString()); return; } @@ -277,7 +277,7 @@ public class McpClientExtensionsTests : ClientServerTestBase IMcpClient client = await CreateMcpClientForServer(); var tool = (await client.ListToolsAsync(emptyOptions, TestContext.Current.CancellationToken)).First(); - await Assert.ThrowsAsync(() => tool.InvokeAsync(new Dictionary { ["i"] = 42 }, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await tool.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken)); } [Fact] diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index 3fbd8f1..baca8f1 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -105,7 +105,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture t.Name == "echo"); - var result = await echo.InvokeAsync([new KeyValuePair("message", "Hello MCP!")], TestContext.Current.CancellationToken); + var result = await echo.InvokeAsync(new() { ["message"] = "Hello MCP!" }, TestContext.Current.CancellationToken); // assert Assert.NotNull(result); @@ -495,7 +495,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture t.Name == "DoubleValue"); - await tool.InvokeAsync(new Dictionary() { ["amount"] = 42 }, TestContext.Current.CancellationToken); + await tool.InvokeAsync(new() { ["amount"] = 42 }, TestContext.Current.CancellationToken); }); } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs index 30d13fd..0962f1b 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs @@ -23,7 +23,6 @@ public class McpServerToolTests Assert.NotNull(McpServerTool.Create(typeof(DisposableToolType).GetMethod(nameof(DisposableToolType.InstanceMethod))!, new DisposableToolType())); Assert.NotNull(McpServerTool.Create(typeof(DisposableToolType).GetMethod(nameof(DisposableToolType.StaticMethod))!)); Assert.Throws("target", () => McpServerTool.Create(typeof(DisposableToolType).GetMethod(nameof(DisposableToolType.InstanceMethod))!, target: null!)); - Assert.Throws("target", () => McpServerTool.Create(typeof(DisposableToolType).GetMethod(nameof(DisposableToolType.StaticMethod))!, new DisposableToolType())); } [Fact]