Skip to content
Merged
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
24 changes: 12 additions & 12 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<System10Version>10.0.0-preview.2.25163.2</System10Version>
<MicrosoftExtensionsAIVersion>9.3.0-preview.1.25161.3</MicrosoftExtensionsAIVersion>
<MicrosoftExtensionsAIVersion>9.4.0-preview.1.25207.5</MicrosoftExtensionsAIVersion>
</PropertyGroup>

<!-- Product dependencies netstandard -->
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageVersion Include="Microsoft.Bcl.Memory" Version="9.0.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageVersion Include="System.IO.Pipelines" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="System.Threading.Channels" Version="8.0.0" />
Expand All @@ -18,15 +18,15 @@
<!-- Product dependencies LTS -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageVersion Include="System.IO.Pipelines" Version="8.0.0" />
</ItemGroup>

<!-- Product dependencies .NET 9 -->
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageVersion Include="System.IO.Pipelines" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
<PackageVersion Include="System.IO.Pipelines" Version="9.0.4" />
</ItemGroup>

<!-- Product dependencies shared -->
Expand All @@ -49,11 +49,11 @@
</PackageVersion>
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="$(MicrosoftExtensionsAIVersion)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="OpenTelemetry" Version="1.11.2" />
Expand Down
2 changes: 1 addition & 1 deletion samples/ChatWithTools/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
// 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.
Expand Down
21 changes: 14 additions & 7 deletions src/ModelContextProtocol/Client/McpClientTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@
using ModelContextProtocol.Utils.Json;
using Microsoft.Extensions.AI;
using System.Text.Json;
using System.Collections.ObjectModel;

namespace ModelContextProtocol.Client;

/// <summary>Provides an AI function that calls a tool through <see cref="IMcpClient"/>.</summary>
public sealed class McpClientTool : AIFunction
{
/// <summary>Additional properties exposed from tools.</summary>
private static readonly ReadOnlyDictionary<string, object?> s_additionalProperties =
new(new Dictionary<string, object?>()
{
["Strict"] = false, // some MCP schemas may not meet "strict" requirements
});

private readonly IMcpClient _client;
private readonly string _name;
private readonly string _description;
Expand Down Expand Up @@ -62,14 +70,13 @@ public McpClientTool WithDescription(string description)
public override JsonSerializerOptions JsonSerializerOptions { get; }

/// <inheritdoc/>
protected async override Task<object?> InvokeCoreAsync(
IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken)
{
IReadOnlyDictionary<string, object?> argDict =
arguments as IReadOnlyDictionary<string, object?> ??
arguments.ToDictionary();
public override IReadOnlyDictionary<string, object?> AdditionalProperties => s_additionalProperties;

CallToolResponse result = await _client.CallToolAsync(ProtocolTool.Name, argDict, JsonSerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
/// <inheritdoc/>
protected async override ValueTask<object?> 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);
}
}
6 changes: 1 addition & 5 deletions src/ModelContextProtocol/ModelContextProtocol.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,10 @@
<!-- Dependencies needed by all -->
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="System.Net.ServerSentEvents" />

<!-- Temporarily removed until new version can be picked up that
has reduced dependencies:
<PackageReference Include="Microsoft.Extensions.AI" />
-->
</ItemGroup>

<ItemGroup>
Expand Down
44 changes: 20 additions & 24 deletions src/ModelContextProtocol/Server/AIFunctionMcpServerPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,10 +11,6 @@ namespace ModelContextProtocol.Server;
/// <summary>Provides an <see cref="McpServerPrompt"/> that's implemented via an <see cref="AIFunction"/>.</summary>
internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
{
/// <summary>Key used temporarily for flowing request context into an AIFunction.</summary>
/// <remarks>This will be replaced with use of AIFunctionArguments.Context.</remarks>
internal const string RequestContextKey = "__temporary_RequestContext";

/// <summary>
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="Delegate"/> instance.
/// </summary>
Expand All @@ -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);
}

Expand All @@ -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<McpServerPromptAttribute>()?.Name,
Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => Task.FromResult(result),
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
ConfigureParameterBinding = pi =>
{
if (pi.ParameterType == typeof(RequestContext<GetPromptRequestParams>))
Expand Down Expand Up @@ -129,9 +117,9 @@ private static TemporaryAIFunctionFactoryOptions CreateAIFunctionFactoryOptions(

return default;

static RequestContext<GetPromptRequestParams>? GetRequestContext(IReadOnlyDictionary<string, object?> args)
static RequestContext<GetPromptRequestParams>? GetRequestContext(AIFunctionArguments args)
{
if (args.TryGetValue(RequestContextKey, out var orc) &&
if (args.Context?.TryGetValue(typeof(RequestContext<GetPromptRequestParams>), out var orc) is true &&
orc is RequestContext<GetPromptRequestParams> requestContext)
{
return requestContext;
Expand Down Expand Up @@ -204,14 +192,22 @@ public override async Task<GetPromptResult> GetAsync(
RequestContext<GetPromptRequestParams> 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<string, object?> 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<object, object?>() { [typeof(RequestContext<GetPromptRequestParams>)] = 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);

Expand Down
45 changes: 20 additions & 25 deletions src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ namespace ModelContextProtocol.Server;
/// <summary>Provides an <see cref="McpServerTool"/> that's implemented via an <see cref="AIFunction"/>.</summary>
internal sealed class AIFunctionMcpServerTool : McpServerTool
{
/// <summary>Key used temporarily for flowing request context into an AIFunction.</summary>
/// <remarks>This will be replaced with use of AIFunctionArguments.Context.</remarks>
internal const string RequestContextKey = "__temporary_RequestContext";

/// <summary>
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
/// </summary>
Expand All @@ -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);
}

Expand All @@ -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<McpServerToolAttribute>()?.Name,
Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => Task.FromResult(result),
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
ConfigureParameterBinding = pi =>
{
if (pi.ParameterType == typeof(RequestContext<CallToolRequestParams>))
Expand Down Expand Up @@ -150,9 +139,9 @@ private static TemporaryAIFunctionFactoryOptions CreateAIFunctionFactoryOptions(

return default;

static RequestContext<CallToolRequestParams>? GetRequestContext(IReadOnlyDictionary<string, object?> args)
static RequestContext<CallToolRequestParams>? GetRequestContext(AIFunctionArguments args)
{
if (args.TryGetValue(RequestContextKey, out var orc) &&
if (args.Context?.TryGetValue(typeof(RequestContext<CallToolRequestParams>), out var orc) is true &&
orc is RequestContext<CallToolRequestParams> requestContext)
{
return requestContext;
Expand Down Expand Up @@ -251,14 +240,22 @@ public override async Task<CallToolResponse> InvokeAsync(
RequestContext<CallToolRequestParams> 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<string, object?> 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<object, object?>() { [typeof(RequestContext<CallToolRequestParams>)] = request }
};

var argDict = request.Params?.Arguments;
if (argDict is not null)
{
foreach (var kvp in argDict)
{
arguments[kvp.Key] = kvp.Value;
}
}

object? result;
try
Expand Down Expand Up @@ -313,8 +310,6 @@ public override async Task<CallToolResponse> InvokeAsync(

CallToolResponse callToolResponse => callToolResponse,

// TODO https://github.com/modelcontextprotocol/csharp-sdk/issues/69:
// Add specialization for annotations.
_ => new()
{
Content = [new()
Expand Down
Loading