Enable injecting IMcpServer and friends into ctors (#570)
This commit is contained in:
parent
9d835a0625
commit
f3539cc2ca
7 changed files with 249 additions and 191 deletions
|
|
@ -2,6 +2,7 @@ using Microsoft.Extensions.AI;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
|
|
@ -57,8 +58,8 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
return Create(
|
||||
AIFunctionFactory.Create(method, args =>
|
||||
{
|
||||
var request = (RequestContext<GetPromptRequestParams>)args.Context![typeof(RequestContext<GetPromptRequestParams>)]!;
|
||||
return createTargetFunc(request);
|
||||
Debug.Assert(args.Services is RequestServiceProvider<GetPromptRequestParams>, $"The service provider should be a {nameof(RequestServiceProvider<GetPromptRequestParams>)} for this method to work correctly.");
|
||||
return createTargetFunc(((RequestServiceProvider<GetPromptRequestParams>)args.Services!).Request);
|
||||
}, CreateAIFunctionFactoryOptions(method, options)),
|
||||
options);
|
||||
}
|
||||
|
|
@ -74,54 +75,15 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
JsonSchemaCreateOptions = options?.SchemaCreateOptions,
|
||||
ConfigureParameterBinding = pi =>
|
||||
{
|
||||
if (pi.ParameterType == typeof(RequestContext<GetPromptRequestParams>))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args),
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IMcpServer))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args)?.Server,
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IProgress<ProgressNotificationValue>))
|
||||
{
|
||||
// Bind IProgress<ProgressNotificationValue> to the progress token in the request,
|
||||
// if there is one. If we can't get one, return a nop progress.
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
{
|
||||
var requestContent = GetRequestContext(args);
|
||||
if (requestContent?.Server is { } server &&
|
||||
requestContent?.Params?.ProgressToken is { } progressToken)
|
||||
{
|
||||
return new TokenProgress(server, progressToken);
|
||||
}
|
||||
|
||||
return NullProgress.Instance;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (options?.Services is { } services &&
|
||||
services.GetService<IServiceProviderIsService>() is { } ispis &&
|
||||
ispis.IsService(pi.ParameterType))
|
||||
if (RequestServiceProvider<GetPromptRequestParams>.IsAugmentedWith(pi.ParameterType) ||
|
||||
(options?.Services?.GetService<IServiceProviderIsService>() is { } ispis &&
|
||||
ispis.IsService(pi.ParameterType)))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ??
|
||||
args.Services?.GetService(pi.ParameterType) ??
|
||||
(pi.HasDefaultValue ? null :
|
||||
throw new ArgumentException("No service of the requested type was found.")),
|
||||
};
|
||||
|
|
@ -133,24 +95,13 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
(GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
|
||||
(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;
|
||||
|
||||
static RequestContext<GetPromptRequestParams>? GetRequestContext(AIFunctionArguments args)
|
||||
{
|
||||
if (args.Context?.TryGetValue(typeof(RequestContext<GetPromptRequestParams>), out var orc) is true &&
|
||||
orc is RequestContext<GetPromptRequestParams> requestContext)
|
||||
{
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -226,14 +177,10 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
Throw.IfNull(request);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
AIFunctionArguments arguments = new()
|
||||
{
|
||||
Services = request.Services,
|
||||
Context = new Dictionary<object, object?>() { [typeof(RequestContext<GetPromptRequestParams>)] = request }
|
||||
};
|
||||
request.Services = new RequestServiceProvider<GetPromptRequestParams>(request, request.Services);
|
||||
AIFunctionArguments arguments = new() { Services = request.Services };
|
||||
|
||||
var argDict = request.Params?.Arguments;
|
||||
if (argDict is not null)
|
||||
if (request.Params?.Arguments is { } argDict)
|
||||
{
|
||||
foreach (var kvp in argDict)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using ModelContextProtocol.Protocol;
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
|
@ -64,8 +65,8 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
return Create(
|
||||
AIFunctionFactory.Create(method, args =>
|
||||
{
|
||||
var request = (RequestContext<ReadResourceRequestParams>)args.Context![typeof(RequestContext<ReadResourceRequestParams>)]!;
|
||||
return createTargetFunc(request);
|
||||
Debug.Assert(args.Services is RequestServiceProvider<ReadResourceRequestParams>, $"The service provider should be a {nameof(RequestServiceProvider<ReadResourceRequestParams>)} for this method to work correctly.");
|
||||
return createTargetFunc(((RequestServiceProvider<ReadResourceRequestParams>)args.Services!).Request);
|
||||
}, CreateAIFunctionFactoryOptions(method, options)),
|
||||
options);
|
||||
}
|
||||
|
|
@ -81,54 +82,15 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
JsonSchemaCreateOptions = options?.SchemaCreateOptions,
|
||||
ConfigureParameterBinding = pi =>
|
||||
{
|
||||
if (pi.ParameterType == typeof(RequestContext<ReadResourceRequestParams>))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args),
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IMcpServer))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args)?.Server,
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IProgress<ProgressNotificationValue>))
|
||||
{
|
||||
// Bind IProgress<ProgressNotificationValue> to the progress token in the request,
|
||||
// if there is one. If we can't get one, return a nop progress.
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
{
|
||||
var requestContent = GetRequestContext(args);
|
||||
if (requestContent?.Server is { } server &&
|
||||
requestContent?.Params?.ProgressToken is { } progressToken)
|
||||
{
|
||||
return new TokenProgress(server, progressToken);
|
||||
}
|
||||
|
||||
return NullProgress.Instance;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (options?.Services is { } services &&
|
||||
services.GetService<IServiceProviderIsService>() is { } ispis &&
|
||||
ispis.IsService(pi.ParameterType))
|
||||
if (RequestServiceProvider<ReadResourceRequestParams>.IsAugmentedWith(pi.ParameterType) ||
|
||||
(options?.Services?.GetService<IServiceProviderIsService>() is { } ispis &&
|
||||
ispis.IsService(pi.ParameterType)))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ??
|
||||
args.Services?.GetService(pi.ParameterType) ??
|
||||
(pi.HasDefaultValue ? null :
|
||||
throw new ArgumentException("No service of the requested type was found.")),
|
||||
};
|
||||
|
|
@ -140,7 +102,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
(GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
|
||||
(args?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
|
||||
(pi.HasDefaultValue ? null :
|
||||
throw new ArgumentException("No service of the requested type was found.")),
|
||||
};
|
||||
|
|
@ -172,17 +134,6 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
}
|
||||
|
||||
return default;
|
||||
|
||||
static RequestContext<ReadResourceRequestParams>? GetRequestContext(AIFunctionArguments args)
|
||||
{
|
||||
if (args.Context?.TryGetValue(typeof(RequestContext<ReadResourceRequestParams>), out var rc) is true &&
|
||||
rc is RequestContext<ReadResourceRequestParams> requestContext)
|
||||
{
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -365,11 +316,8 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
}
|
||||
|
||||
// Build up the arguments for the AIFunction call, including all of the name/value pairs from the URI.
|
||||
AIFunctionArguments arguments = new()
|
||||
{
|
||||
Services = request.Services,
|
||||
Context = new Dictionary<object, object?>() { [typeof(RequestContext<ReadResourceRequestParams>)] = request }
|
||||
};
|
||||
request.Services = new RequestServiceProvider<ReadResourceRequestParams>(request, request.Services);
|
||||
AIFunctionArguments arguments = new() { Services = request.Services };
|
||||
|
||||
// For templates, populate the arguments from the URI template.
|
||||
if (match is not null)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging;
|
|||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
|
@ -64,8 +64,8 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
return Create(
|
||||
AIFunctionFactory.Create(method, args =>
|
||||
{
|
||||
var request = (RequestContext<CallToolRequestParams>)args.Context![typeof(RequestContext<CallToolRequestParams>)]!;
|
||||
return createTargetFunc(request);
|
||||
Debug.Assert(args.Services is RequestServiceProvider<CallToolRequestParams>, $"The service provider should be a {nameof(RequestServiceProvider<CallToolRequestParams>)} for this method to work correctly.");
|
||||
return createTargetFunc(((RequestServiceProvider<CallToolRequestParams>)args.Services!).Request);
|
||||
}, CreateAIFunctionFactoryOptions(method, options)),
|
||||
options);
|
||||
}
|
||||
|
|
@ -81,54 +81,15 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
JsonSchemaCreateOptions = options?.SchemaCreateOptions,
|
||||
ConfigureParameterBinding = pi =>
|
||||
{
|
||||
if (pi.ParameterType == typeof(RequestContext<CallToolRequestParams>))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args),
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IMcpServer))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) => GetRequestContext(args)?.Server,
|
||||
};
|
||||
}
|
||||
|
||||
if (pi.ParameterType == typeof(IProgress<ProgressNotificationValue>))
|
||||
{
|
||||
// Bind IProgress<ProgressNotificationValue> to the progress token in the request,
|
||||
// if there is one. If we can't get one, return a nop progress.
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
{
|
||||
var requestContent = GetRequestContext(args);
|
||||
if (requestContent?.Server is { } server &&
|
||||
requestContent?.Params?.ProgressToken is { } progressToken)
|
||||
{
|
||||
return new TokenProgress(server, progressToken);
|
||||
}
|
||||
|
||||
return NullProgress.Instance;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (options?.Services is { } services &&
|
||||
services.GetService<IServiceProviderIsService>() is { } ispis &&
|
||||
ispis.IsService(pi.ParameterType))
|
||||
if (RequestServiceProvider<CallToolRequestParams>.IsAugmentedWith(pi.ParameterType) ||
|
||||
(options?.Services?.GetService<IServiceProviderIsService>() is { } ispis &&
|
||||
ispis.IsService(pi.ParameterType)))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ??
|
||||
args.Services?.GetService(pi.ParameterType) ??
|
||||
(pi.HasDefaultValue ? null :
|
||||
throw new ArgumentException("No service of the requested type was found.")),
|
||||
};
|
||||
|
|
@ -140,24 +101,13 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
{
|
||||
ExcludeFromSchema = true,
|
||||
BindParameter = (pi, args) =>
|
||||
(GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ??
|
||||
(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;
|
||||
|
||||
static RequestContext<CallToolRequestParams>? GetRequestContext(AIFunctionArguments args)
|
||||
{
|
||||
if (args.Context?.TryGetValue(typeof(RequestContext<CallToolRequestParams>), out var orc) is true &&
|
||||
orc is RequestContext<CallToolRequestParams> requestContext)
|
||||
{
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -260,14 +210,10 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
Throw.IfNull(request);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
AIFunctionArguments arguments = new()
|
||||
{
|
||||
Services = request.Services,
|
||||
Context = new Dictionary<object, object?>() { [typeof(RequestContext<CallToolRequestParams>)] = request }
|
||||
};
|
||||
request.Services = new RequestServiceProvider<CallToolRequestParams>(request, request.Services);
|
||||
AIFunctionArguments arguments = new() { Services = request.Services };
|
||||
|
||||
var argDict = request.Params?.Arguments;
|
||||
if (argDict is not null)
|
||||
if (request.Params?.Arguments is { } argDict)
|
||||
{
|
||||
foreach (var kvp in argDict)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>Augments a service provider with additional request-related services.</summary>
|
||||
internal sealed class RequestServiceProvider<TRequestParams>(
|
||||
RequestContext<TRequestParams> request, IServiceProvider? innerServices) :
|
||||
IServiceProvider, IKeyedServiceProvider,
|
||||
IServiceProviderIsService, IServiceProviderIsKeyedService,
|
||||
IDisposable, IAsyncDisposable
|
||||
where TRequestParams : RequestParams
|
||||
{
|
||||
/// <summary>Gets the request associated with this instance.</summary>
|
||||
public RequestContext<TRequestParams> Request => request;
|
||||
|
||||
/// <summary>Gets whether the specified type is in the list of additional types this service provider wraps around the one in a provided request's services.</summary>
|
||||
public static bool IsAugmentedWith(Type serviceType) =>
|
||||
serviceType == typeof(RequestContext<TRequestParams>) ||
|
||||
serviceType == typeof(IMcpServer) ||
|
||||
serviceType == typeof(IProgress<ProgressNotificationValue>);
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? GetService(Type serviceType) =>
|
||||
serviceType == typeof(RequestContext<TRequestParams>) ? request :
|
||||
serviceType == typeof(IMcpServer) ? request.Server :
|
||||
serviceType == typeof(IProgress<ProgressNotificationValue>) ?
|
||||
(request.Params?.ProgressToken is { } progressToken ? new TokenProgress(request.Server, progressToken) : NullProgress.Instance) :
|
||||
innerServices?.GetService(serviceType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsService(Type serviceType) =>
|
||||
IsAugmentedWith(serviceType) ||
|
||||
(innerServices as IServiceProviderIsService)?.IsService(serviceType) is true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsKeyedService(Type serviceType, object? serviceKey) =>
|
||||
(serviceKey is null && IsService(serviceType)) ||
|
||||
(innerServices as IServiceProviderIsKeyedService)?.IsKeyedService(serviceType, serviceKey) is true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? GetKeyedService(Type serviceType, object? serviceKey) =>
|
||||
serviceKey is null ? GetService(serviceType) :
|
||||
(innerServices as IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey);
|
||||
|
||||
/// <inheritdoc />
|
||||
public object GetRequiredKeyedService(Type serviceType, object? serviceKey) =>
|
||||
GetKeyedService(serviceType, serviceKey) ??
|
||||
throw new InvalidOperationException($"No service of type '{serviceType}' with key '{serviceKey}' is registered.");
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() =>
|
||||
(innerServices as IDisposable)?.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync() =>
|
||||
innerServices is IAsyncDisposable asyncDisposable ? asyncDisposable.DisposeAsync() : default;
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using Moq;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
|
@ -44,6 +46,58 @@ public class McpServerPromptTests
|
|||
Assert.Equal("Hello", Assert.IsType<TextContentBlock>(result.Messages[0].Content).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SupportsCtorInjection()
|
||||
{
|
||||
MyService expectedMyService = new();
|
||||
|
||||
ServiceCollection sc = new();
|
||||
sc.AddSingleton(expectedMyService);
|
||||
IServiceProvider services = sc.BuildServiceProvider();
|
||||
|
||||
Mock<IMcpServer> mockServer = new();
|
||||
mockServer.SetupGet(s => s.Services).Returns(services);
|
||||
|
||||
MethodInfo? testMethod = typeof(HasCtorWithSpecialParameters).GetMethod(nameof(HasCtorWithSpecialParameters.TestPrompt));
|
||||
Assert.NotNull(testMethod);
|
||||
McpServerPrompt prompt = McpServerPrompt.Create(testMethod, r =>
|
||||
{
|
||||
Assert.NotNull(r.Services);
|
||||
return ActivatorUtilities.CreateInstance(r.Services, typeof(HasCtorWithSpecialParameters));
|
||||
}, new() { Services = services });
|
||||
|
||||
var result = await prompt.GetAsync(
|
||||
new RequestContext<GetPromptRequestParams>(mockServer.Object),
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Messages);
|
||||
Assert.Single(result.Messages);
|
||||
Assert.Equal("True True True True", Assert.IsType<TextContentBlock>(result.Messages[0].Content).Text);
|
||||
}
|
||||
|
||||
private sealed class HasCtorWithSpecialParameters
|
||||
{
|
||||
private readonly MyService _ms;
|
||||
private readonly IMcpServer _server;
|
||||
private readonly RequestContext<GetPromptRequestParams> _request;
|
||||
private readonly IProgress<ProgressNotificationValue> _progress;
|
||||
|
||||
public HasCtorWithSpecialParameters(MyService ms, IMcpServer server, RequestContext<GetPromptRequestParams> request, IProgress<ProgressNotificationValue> progress)
|
||||
{
|
||||
Assert.NotNull(ms);
|
||||
Assert.NotNull(server);
|
||||
Assert.NotNull(request);
|
||||
Assert.NotNull(progress);
|
||||
|
||||
_ms = ms;
|
||||
_server = server;
|
||||
_request = request;
|
||||
_progress = progress;
|
||||
}
|
||||
|
||||
public string TestPrompt() => $"{_ms is not null} {_server is not null} {_request is not null} {_progress is not null}";
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SupportsServiceFromDI()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -360,6 +360,59 @@ public partial class McpServerResourceTests
|
|||
Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SupportsCtorInjection()
|
||||
{
|
||||
MyService expectedMyService = new();
|
||||
|
||||
ServiceCollection sc = new();
|
||||
sc.AddSingleton(expectedMyService);
|
||||
IServiceProvider services = sc.BuildServiceProvider();
|
||||
|
||||
Mock<IMcpServer> mockServer = new();
|
||||
mockServer.SetupGet(s => s.Services).Returns(services);
|
||||
|
||||
MethodInfo? testMethod = typeof(HasCtorWithSpecialParameters).GetMethod(nameof(HasCtorWithSpecialParameters.TestResource));
|
||||
Assert.NotNull(testMethod);
|
||||
McpServerResource tool = McpServerResource.Create(testMethod, r =>
|
||||
{
|
||||
Assert.NotNull(r.Services);
|
||||
return ActivatorUtilities.CreateInstance(r.Services, typeof(HasCtorWithSpecialParameters));
|
||||
}, new() { Services = services });
|
||||
|
||||
var result = await tool.ReadAsync(
|
||||
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "https://something" } },
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Contents);
|
||||
Assert.Single(result.Contents);
|
||||
Assert.Equal("True True True True", Assert.IsType<TextResourceContents>(result.Contents[0]).Text);
|
||||
}
|
||||
|
||||
private sealed class HasCtorWithSpecialParameters
|
||||
{
|
||||
private readonly MyService _ms;
|
||||
private readonly IMcpServer _server;
|
||||
private readonly RequestContext<ReadResourceRequestParams> _request;
|
||||
private readonly IProgress<ProgressNotificationValue> _progress;
|
||||
|
||||
public HasCtorWithSpecialParameters(MyService ms, IMcpServer server, RequestContext<ReadResourceRequestParams> request, IProgress<ProgressNotificationValue> progress)
|
||||
{
|
||||
Assert.NotNull(ms);
|
||||
Assert.NotNull(server);
|
||||
Assert.NotNull(request);
|
||||
Assert.NotNull(progress);
|
||||
|
||||
_ms = ms;
|
||||
_server = server;
|
||||
_request = request;
|
||||
_progress = progress;
|
||||
}
|
||||
|
||||
[McpServerResource(UriTemplate = "https://something")]
|
||||
public string TestResource() => $"{_ms is not null} {_server is not null} {_request is not null} {_progress is not null}";
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ServiceLifetime.Singleton)]
|
||||
[InlineData(ServiceLifetime.Scoped)]
|
||||
|
|
|
|||
|
|
@ -50,6 +50,58 @@ public partial class McpServerToolTests
|
|||
Assert.Equal("42", (result.Content[0] as TextContentBlock)?.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SupportsCtorInjection()
|
||||
{
|
||||
MyService expectedMyService = new();
|
||||
|
||||
ServiceCollection sc = new();
|
||||
sc.AddSingleton(expectedMyService);
|
||||
IServiceProvider services = sc.BuildServiceProvider();
|
||||
|
||||
Mock<IMcpServer> mockServer = new();
|
||||
mockServer.SetupGet(s => s.Services).Returns(services);
|
||||
|
||||
MethodInfo? testMethod = typeof(HasCtorWithSpecialParameters).GetMethod(nameof(HasCtorWithSpecialParameters.TestTool));
|
||||
Assert.NotNull(testMethod);
|
||||
McpServerTool tool = McpServerTool.Create(testMethod, r =>
|
||||
{
|
||||
Assert.NotNull(r.Services);
|
||||
return ActivatorUtilities.CreateInstance(r.Services, typeof(HasCtorWithSpecialParameters));
|
||||
}, new() { Services = services });
|
||||
|
||||
var result = await tool.InvokeAsync(
|
||||
new RequestContext<CallToolRequestParams>(mockServer.Object),
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Content);
|
||||
Assert.Single(result.Content);
|
||||
Assert.Equal("True True True True", Assert.IsType<TextContentBlock>(result.Content[0]).Text);
|
||||
}
|
||||
|
||||
private sealed class HasCtorWithSpecialParameters
|
||||
{
|
||||
private readonly MyService _ms;
|
||||
private readonly IMcpServer _server;
|
||||
private readonly RequestContext<CallToolRequestParams> _request;
|
||||
private readonly IProgress<ProgressNotificationValue> _progress;
|
||||
|
||||
public HasCtorWithSpecialParameters(MyService ms, IMcpServer server, RequestContext<CallToolRequestParams> request, IProgress<ProgressNotificationValue> progress)
|
||||
{
|
||||
Assert.NotNull(ms);
|
||||
Assert.NotNull(server);
|
||||
Assert.NotNull(request);
|
||||
Assert.NotNull(progress);
|
||||
|
||||
_ms = ms;
|
||||
_server = server;
|
||||
_request = request;
|
||||
_progress = progress;
|
||||
}
|
||||
|
||||
public string TestTool() => $"{_ms is not null} {_server is not null} {_request is not null} {_progress is not null}";
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ServiceLifetime.Singleton)]
|
||||
[InlineData(ServiceLifetime.Scoped)]
|
||||
|
|
|
|||
Loading…
Reference in a new issue