Enable injecting IMcpServer and friends into ctors (#570)

This commit is contained in:
Stephen Toub 2025-06-30 10:58:08 -04:00 • committed by GitHub
commit f3539cc2ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 249 additions and 191 deletions

View file

@ -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)
{

View file

@ -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)

View file

@ -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)
{

View file

@ -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;
}