Update to latest M.E.AI version (#401)

This commit is contained in:
Stephen Toub 2025-05-10 07:53:04 -04:00 • committed by GitHub
commit 61092e0266
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 106 additions and 10 deletions

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<System10Version>10.0.0-preview.3.25171.5</System10Version> <System10Version>10.0.0-preview.3.25171.5</System10Version>
<MicrosoftExtensionsAIVersion>9.4.3-preview.1.25230.7</MicrosoftExtensionsAIVersion> <MicrosoftExtensionsAIVersion>9.4.4-preview.1.25259.16</MicrosoftExtensionsAIVersion>
</PropertyGroup> </PropertyGroup>
<!-- Product dependencies netstandard --> <!-- Product dependencies netstandard -->
@ -31,7 +31,7 @@
<!-- Product dependencies shared --> <!-- Product dependencies shared -->
<ItemGroup> <ItemGroup>
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="$(MicrosoftExtensionsAIVersion)" /> <PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.4.4-preview.1.25259.16" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="$(MicrosoftExtensionsAIVersion)" /> <PackageVersion Include="Microsoft.Extensions.AI" Version="$(MicrosoftExtensionsAIVersion)" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="$(System10Version)" /> <PackageVersion Include="System.Net.ServerSentEvents" Version="$(System10Version)" />
</ItemGroup> </ItemGroup>

View file

@ -34,7 +34,6 @@
<!-- Dependencies needed by all --> <!-- Dependencies needed by all -->
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" /> <PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="System.Net.ServerSentEvents" /> <PackageReference Include="System.Net.ServerSentEvents" />

View file

@ -1,4 +1,5 @@
using Microsoft.Extensions.AI; using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils; using ModelContextProtocol.Utils;
using ModelContextProtocol.Utils.Json; using ModelContextProtocol.Utils.Json;
@ -68,7 +69,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
Description = options?.Description, Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result), MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
Services = options?.Services, CreateInstance = AIFunctionMcpServerTool.GetCreateInstanceFunc(),
ConfigureParameterBinding = pi => ConfigureParameterBinding = pi =>
{ {
if (pi.ParameterType == typeof(RequestContext<GetPromptRequestParams>)) if (pi.ParameterType == typeof(RequestContext<GetPromptRequestParams>))
@ -110,6 +111,32 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
}; };
} }
if (options?.Services is { } services &&
services.GetService<IServiceProviderIsService>() is { } ispis &&
ispis.IsService(pi.ParameterType))
{
return new()
{
ExcludeFromSchema = true,
BindParameter = (pi, args) =>
GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
};
}
if (pi.GetCustomAttribute<FromKeyedServicesAttribute>() is { } keyedAttr)
{
return new()
{
ExcludeFromSchema = true,
BindParameter = (pi, args) =>
(GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
};
}
return default; return default;
static RequestContext<GetPromptRequestParams>? GetRequestContext(AIFunctionArguments args) static RequestContext<GetPromptRequestParams>? GetRequestContext(AIFunctionArguments args)

View file

@ -1,10 +1,10 @@
using Microsoft.Extensions.AI; using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils; using ModelContextProtocol.Utils;
using ModelContextProtocol.Utils.Json; using ModelContextProtocol.Utils.Json;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Reflection; using System.Reflection;
@ -76,7 +76,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
Description = options?.Description, Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result), MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
SerializerOptions = McpJsonUtilities.DefaultOptions, SerializerOptions = McpJsonUtilities.DefaultOptions,
Services = options?.Services, CreateInstance = AIFunctionMcpServerTool.GetCreateInstanceFunc(),
ConfigureParameterBinding = pi => ConfigureParameterBinding = pi =>
{ {
if (pi.ParameterType == typeof(RequestContext<ReadResourceRequestParams>)) if (pi.ParameterType == typeof(RequestContext<ReadResourceRequestParams>))
@ -118,6 +118,32 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
}; };
} }
if (options?.Services is { } services &&
services.GetService<IServiceProviderIsService>() is { } ispis &&
ispis.IsService(pi.ParameterType))
{
return new()
{
ExcludeFromSchema = true,
BindParameter = (pi, args) =>
GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
};
}
if (pi.GetCustomAttribute<FromKeyedServicesAttribute>() is { } keyedAttr)
{
return new()
{
ExcludeFromSchema = true,
BindParameter = (pi, args) =>
(GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
};
}
// These parameters are the ones and only ones to include in the schema. The schema // These parameters are the ones and only ones to include in the schema. The schema
// won't be consumed by anyone other than this instance, which will use it to determine // won't be consumed by anyone other than this instance, which will use it to determine
// which properties should show up in the URI template. // which properties should show up in the URI template.

View file

@ -1,4 +1,5 @@
using Microsoft.Extensions.AI; using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils; using ModelContextProtocol.Utils;
using ModelContextProtocol.Utils.Json; using ModelContextProtocol.Utils.Json;
@ -60,6 +61,14 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
options); options);
} }
// TODO: Fix the need for this suppression.
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111:ReflectionToDynamicallyAccessedMembers",
Justification = "AIFunctionFactory ensures that the Type passed to AIFunctionFactoryOptions.CreateInstance has public constructors preserved")]
internal static Func<Type, AIFunctionArguments, object> GetCreateInstanceFunc() =>
static ([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] type, args) => args.Services is { } services ?
ActivatorUtilities.CreateInstance(services, type) :
Activator.CreateInstance(type)!;
private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
MethodInfo method, McpServerToolCreateOptions? options) => MethodInfo method, McpServerToolCreateOptions? options) =>
new() new()
@ -68,7 +77,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
Description = options?.Description, Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result), MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
Services = options?.Services, CreateInstance = GetCreateInstanceFunc(),
ConfigureParameterBinding = pi => ConfigureParameterBinding = pi =>
{ {
if (pi.ParameterType == typeof(RequestContext<CallToolRequestParams>)) if (pi.ParameterType == typeof(RequestContext<CallToolRequestParams>))
@ -110,6 +119,32 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
}; };
} }
if (options?.Services is { } services &&
services.GetService<IServiceProviderIsService>() is { } ispis &&
ispis.IsService(pi.ParameterType))
{
return new()
{
ExcludeFromSchema = true,
BindParameter = (pi, args) =>
GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
};
}
if (pi.GetCustomAttribute<FromKeyedServicesAttribute>() is { } keyedAttr)
{
return new()
{
ExcludeFromSchema = true,
BindParameter = (pi, args) =>
(GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
(pi.HasDefaultValue ? null :
throw new ArgumentException("No service of the requested type was found.")),
};
}
return default; return default;
static RequestContext<CallToolRequestParams>? GetRequestContext(AIFunctionArguments args) static RequestContext<CallToolRequestParams>? GetRequestContext(AIFunctionArguments args)

View file

@ -60,7 +60,7 @@ public class McpServerPromptTests
Assert.Contains("something", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []); Assert.Contains("something", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []);
Assert.DoesNotContain("actualMyService", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []); Assert.DoesNotContain("actualMyService", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []);
await Assert.ThrowsAsync<ArgumentNullException>(async () => await prompt.GetAsync( await Assert.ThrowsAnyAsync<ArgumentException>(async () => await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object), new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken)); TestContext.Current.CancellationToken));

View file

@ -375,7 +375,7 @@ public partial class McpServerResourceTests
Mock<IMcpServer> mockServer = new(); Mock<IMcpServer> mockServer = new();
await Assert.ThrowsAsync<ArgumentNullException>(async () => await resource.ReadAsync( await Assert.ThrowsAnyAsync<ArgumentException>(async () => await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } }, new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken)); TestContext.Current.CancellationToken));

View file

@ -156,6 +156,10 @@ public partial class McpServerToolTests
[Fact] [Fact]
public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposableTargets() public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposableTargets()
{ {
ServiceCollection sc = new();
sc.AddSingleton<MyService>();
IServiceProvider services = sc.BuildServiceProvider();
McpServerToolCreateOptions options = new() { SerializerOptions = JsonContext2.Default.Options }; McpServerToolCreateOptions options = new() { SerializerOptions = JsonContext2.Default.Options };
McpServerTool tool1 = McpServerTool.Create( McpServerTool tool1 = McpServerTool.Create(
typeof(AsyncDisposableAndDisposableToolType).GetMethod(nameof(AsyncDisposableAndDisposableToolType.InstanceMethod))!, typeof(AsyncDisposableAndDisposableToolType).GetMethod(nameof(AsyncDisposableAndDisposableToolType.InstanceMethod))!,
@ -163,7 +167,7 @@ public partial class McpServerToolTests
options); options);
var result = await tool1.InvokeAsync( var result = await tool1.InvokeAsync(
new RequestContext<CallToolRequestParams>(new Mock<IMcpServer>().Object), new RequestContext<CallToolRequestParams>(new Mock<IMcpServer>().Object) { Services = services },
TestContext.Current.CancellationToken); TestContext.Current.CancellationToken);
Assert.Equal("""{"asyncDisposals":1,"disposals":0}""", result.Content[0].Text); Assert.Equal("""{"asyncDisposals":1,"disposals":0}""", result.Content[0].Text);
} }
@ -428,6 +432,11 @@ public partial class McpServerToolTests
private class AsyncDisposableAndDisposableToolType : IAsyncDisposable, IDisposable private class AsyncDisposableAndDisposableToolType : IAsyncDisposable, IDisposable
{ {
public AsyncDisposableAndDisposableToolType(MyService service)
{
Assert.NotNull(service);
}
[JsonPropertyOrder(0)] [JsonPropertyOrder(0)]
public int AsyncDisposals { get; private set; } public int AsyncDisposals { get; private set; }