Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
<MicrosoftExtensionsAIVersion>9.7.1</MicrosoftExtensionsAIVersion>
</PropertyGroup>

<!-- Product dependencies .NET Framework 4.7.2 -->
<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
</ItemGroup>

<!-- Product dependencies netstandard -->
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageVersion Include="Microsoft.Bcl.Memory" Version="$(System9Version)" />
Expand All @@ -20,6 +25,7 @@
<!-- Product dependencies LTS -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.15" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageVersion Include="System.IO.Pipelines" Version="8.0.0" />
Expand All @@ -29,6 +35,7 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(System9Version)" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.9.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(System9Version)" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(System9Version)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(System9Version)" />
<PackageVersion Include="System.IO.Pipelines" Version="$(System9Version)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
ConfigureParameterBinding = pi =>
{
if (RequestServiceProvider<GetPromptRequestParams>.IsAugmentedWith(pi.ParameterType) ||
(options?.Services?.GetService<IServiceProviderIsService>() is { } ispis &&
ispis.IsService(pi.ParameterType)))
(options?.Services?.GetService<IServiceProviderIsService>() is { } serviceProviderIsService &&
serviceProviderIsService.IsService(pi.ParameterType)))
{
return new()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
ConfigureParameterBinding = pi =>
{
if (RequestServiceProvider<ReadResourceRequestParams>.IsAugmentedWith(pi.ParameterType) ||
(options?.Services?.GetService<IServiceProviderIsService>() is { } ispis &&
ispis.IsService(pi.ParameterType)))
(options?.Services?.GetService<IServiceProviderIsService>() is { } serviceProviderIsService &&
serviceProviderIsService.IsService(pi.ParameterType)))
{
return new()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
ConfigureParameterBinding = pi =>
{
if (RequestServiceProvider<CallToolRequestParams>.IsAugmentedWith(pi.ParameterType) ||
(options?.Services?.GetService<IServiceProviderIsService>() is { } ispis &&
ispis.IsService(pi.ParameterType)))
(options?.Services?.GetService<IServiceProviderIsService>() is { } serviceProviderIsService &&
serviceProviderIsService.IsService(pi.ParameterType)))
{
return new()
{
Expand Down
16 changes: 13 additions & 3 deletions src/ModelContextProtocol/McpServerBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -783,8 +783,18 @@ private static void AddSingleSessionServerDependencies(IServiceCollection servic
/// <summary>Creates an instance of the target object.</summary>
private static object CreateTarget(
IServiceProvider? services,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) =>
services is not null ? ActivatorUtilities.CreateInstance(services, type) :
Activator.CreateInstance(type)!;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
{
if (services is null)
{
return Activator.CreateInstance(type)!;
}

var isService = services.GetService<IServiceProviderIsService>()?.IsService(type) == true;

return isService
? services.GetRequiredService(type)
: ActivatorUtilities.CreateInstance(services, type);
Comment on lines +793 to +797
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephentoub and I previously discussed whether or not we should try to resolve [McpServerToolType]'s directly from the DI container before using ActivatorUtilites. This is not something we do for ASP.NET Core MVC Controllers, but it is something we do for SignalR Hubs.

I can't find the conversation right now, but I remember recommending against resolving the tool/resource/prompt type from DI, because resolving SignalR Hubs from DI has been a source of bugs in SignalR in the past, and I hadn't heard people ask for MVC Controllers to get resolved directly from DI either.

I didn't consider the typed AddHttpClient scenario specifically, but MCP handlers are no different than MVC Controllers in this regard. Just as with Controllers, you have to inject the typed client into your MCP handler rather than make the MCP handler a typed client itself.

However, unlike MVC and SignalR, MCP handlers don't have currently have stateful properties like Controller.HttpContext or Hub.Clients.Caller. As it stands, it could be a perf optimization to register your handlers as singletons. The downside would be if we introduced a new handler base type in the future to make things like IMcpServer more discoverable inside handlers.

If we decide to go with this change, I think this could be simplified to the following since we always immediately resolve the service if it exists, and we're not trying to throw if the type is missing.

Suggested change
var isService = services.GetService<IServiceProviderIsService>()?.IsService(type) == true;
return isService
? services.GetRequiredService(type)
: ActivatorUtilities.CreateInstance(services, type);
return services.GetService(type) ?? ActivatorUtilities.CreateInstance(services, type);

GitHub appears to be messing with the diff, but the above should replace lines 793-797.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost forgot we merged #570 which allows you to inject the current IMcpServer into constructors and not just the handler method. If someone were to inject an IMcpServer into their MCP handler type and register it as a singleton, they would run into problems.

}
#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.ComponentModel;

namespace ModelContextProtocol.Tests.Configuration;

public class McpServerBuilderExtensionsCreateTargetHelperTests(ITestOutputHelper testOutputHelper)
: ClientServerTestBase(testOutputHelper)
{
private const string ToolName = "Pets Service";
private const string BaseAddress = "https://localhost:7387/pets";

protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
mcpServerBuilder.WithTools<PetsService>();

services.AddHttpClient<PetsService>(client =>
{
client.BaseAddress = new Uri(BaseAddress);
});
}

/// <summary>
/// Verifies that a typed HttpClient registered in DI is correctly injected into a server tool
/// and used by the tool implementation. This test covers the scenario described in
/// https://github.com/modelcontextprotocol/csharp-sdk/issues/685.
/// </summary>
[Fact]
public async Task Typed_HttpClient_Is_Used_By_Tool()
{
// Arrange
await using var client = await CreateMcpClientForServer();

// Act
var result = await client.CallToolAsync(ToolName, cancellationToken: TestContext.Current.CancellationToken);

// Assert
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);

var text = (result.Content[0] as TextContentBlock)?.Text;
Assert.Equal(BaseAddress, text);
}

[McpServerToolType]
private sealed class PetsService(HttpClient httpClient)
{
[McpServerTool(Name = ToolName)]
[Description("List all pets")]
public string GetBaseAddress()
{
// Returning HttpClient.BaseAddress for verification
return httpClient.BaseAddress?.ToString() ?? string.Empty;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
</PackageReference>
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
Expand Down