Integrate Microsoft.Extensions.AI as a foundational building block (#94)

* Integrate Microsoft.Extensions.AI as a foundational building block

As with logging abstractions coming from M.E.Logging and DI abstractions coming from M.E.DependencyInjection, M.E.AI provides the relevant AI abstractions.

- Adds AsAIFunction and GetAIFunctionsAsync methods for getting functions for tools via a client.
- Uses AIFunctionFactory to implement McpServerBuilderExtensions.WithTools.
- Adds a RequestSamplingAsync overload that accepts ChatMessages/Options.
- Adds an AsSamplingChatClient method that provides an IChatClient usable on the server to request sampling of the client.
- Adds a CreateSamplingHandler method to enable an IChatClient to be easily used to satisfy sampling requests on the client.
- Deprecate McpDotNet.Extensions.AI.

* Address PR feedback
This commit is contained in:
Stephen Toub 2025-03-17 16:34:00 -04:00 • committed by GitHub
commit 4597bbd7c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,6 +1,9 @@
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Types;
using McpDotNet.Utils;
using Microsoft.Extensions.AI;
using System.Runtime.CompilerServices;
using System.Text;
namespace McpDotNet.Server;
@ -8,8 +11,10 @@ namespace McpDotNet.Server;
public static class McpServerExtensions
{
/// <summary>
/// Requests the client to create a new message.
/// Requests to sample an LLM via the client.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">The client does not support sampling.</exception>
public static Task<CreateMessageResult> RequestSamplingAsync(
this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken)
{
@ -17,7 +22,7 @@ public static class McpServerExtensions
if (server.ClientCapabilities?.Sampling is null)
{
throw new McpServerException("Client does not support sampling");
throw new ArgumentException("Client connected to the server does not support sampling.", nameof(server));
}
return server.SendRequestAsync<CreateMessageResult>(
@ -25,9 +30,161 @@ public static class McpServerExtensions
cancellationToken);
}
/// <summary>
/// Requests to sample an LLM via the client.
/// </summary>
/// <param name="server">The server issueing the request.</param>
/// <param name="messages">The messages to send as part of the request.</param>
/// <param name="options">The options to use for the request.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the response from the client.</returns>
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">The client does not support sampling.</exception>
public static async Task<ChatResponse> RequestSamplingAsync(
this IMcpServer server,
IEnumerable<ChatMessage> messages, ChatOptions? options = default, CancellationToken cancellationToken = default)
{
Throw.IfNull(server);
Throw.IfNull(messages);
StringBuilder? systemPrompt = null;
List<SamplingMessage> samplingMessages = [];
foreach (var message in messages)
{
if (message.Role == ChatRole.System)
{
if (systemPrompt is null)
{
systemPrompt = new();
}
else
{
systemPrompt.AppendLine();
}
systemPrompt.Append(message.Text);
continue;
}
if (message.Role == ChatRole.User || message.Role == ChatRole.Assistant)
{
Role role = message.Role == ChatRole.User ? Role.User : Role.Assistant;
foreach (var content in message.Contents)
{
switch (content)
{
case TextContent textContent:
samplingMessages.Add(new()
{
Role = role,
Content = new()
{
Type = "text",
Text = textContent.Text,
},
});
break;
case DataContent dataContent when dataContent.HasTopLevelMediaType("image"):
samplingMessages.Add(new()
{
Role = role,
Content = new()
{
Type = "image",
MimeType = dataContent.MediaType,
Data = Convert.ToBase64String(dataContent.Data.
#if NET
Span),
#else
ToArray()),
#endif
},
});
break;
}
}
}
}
ModelPreferences? modelPreferences = null;
if (options?.ModelId is { } modelId)
{
modelPreferences = new() { Hints = [new() { Name = modelId }] };
}
var result = await server.RequestSamplingAsync(new()
{
Messages = samplingMessages,
MaxTokens = options?.MaxOutputTokens,
StopSequences = options?.StopSequences?.ToArray(),
SystemPrompt = systemPrompt?.ToString(),
Temperature = options?.Temperature,
ModelPreferences = modelPreferences,
}, cancellationToken).ConfigureAwait(false);
ChatMessage responseMessage = new()
{
Role = result.Role == "user" ? ChatRole.User : ChatRole.Assistant
};
if (result.Content is { Type: "text" })
{
responseMessage.Contents.Add(new TextContent(result.Content.Text));
}
else if (result.Content is { Type: "image", MimeType: not null, Data: not null })
{
responseMessage.Contents.Add(new DataContent(Convert.FromBase64String(result.Content.Data), result.Content.MimeType));
}
else if (result.Content is { Type: "resource" } && result.Content.Resource is { } resourceContents)
{
if (resourceContents.Text is not null)
{
responseMessage.Contents.Add(new TextContent(resourceContents.Text));
}
if (resourceContents.Blob is not null && resourceContents.MimeType is not null)
{
responseMessage.Contents.Add(new DataContent(Convert.FromBase64String(resourceContents.Blob), resourceContents.MimeType));
}
}
return new(responseMessage)
{
ModelId = result.Model,
FinishReason = result.StopReason switch
{
"maxTokens" => ChatFinishReason.Length,
"endTurn" or "stopSequence" or _ => ChatFinishReason.Stop,
}
};
}
/// <summary>Creates an <see cref="IChatClient"/> that can be used to send sampling requests to the client.</summary>
/// <param name="server">The server to be wrapped as an <see cref="IChatClient"/>.</param>
/// <returns>The <see cref="IChatClient"/> that can be used to issue sampling requests to the client.</returns>
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">The client does not support sampling.</exception>
public static IChatClient AsSamplingChatClient(this IMcpServer server)
{
Throw.IfNull(server);
if (server.ClientCapabilities?.Sampling is null)
{
throw new ArgumentException("Client connected to the server does not support sampling.", nameof(server));
}
return new SamplingChatClient(server);
}
/// <summary>
/// Requests the client to list the roots it exposes.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">The client does not support roots.</exception>
public static Task<ListRootsResult> RequestRootsAsync(
this IMcpServer server, ListRootsRequestParams request, CancellationToken cancellationToken)
{
@ -35,11 +192,46 @@ public static class McpServerExtensions
if (server.ClientCapabilities?.Roots is null)
{
throw new McpServerException("Client does not support roots");
throw new ArgumentException("Client connected to the server does not support roots.", nameof(server));
}
return server.SendRequestAsync<ListRootsResult>(
new JsonRpcRequest { Method = "roots/list", Params = request },
cancellationToken);
}
/// <summary>Provides an <see cref="IChatClient"/> implementation that's implemented via client sampling.</summary>
/// <param name="server"></param>
private sealed class SamplingChatClient(IMcpServer server) : IChatClient
{
/// <inheritdoc/>
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
server.RequestSamplingAsync(messages, options, cancellationToken);
/// <inheritdoc/>
async IAsyncEnumerable<ChatResponseUpdate> IChatClient.GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var response = await GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
foreach (var update in response.ToChatResponseUpdates())
{
yield return update;
}
}
/// <inheritdoc/>
object? IChatClient.GetService(Type serviceType, object? serviceKey)
{
Throw.IfNull(serviceType);
return
serviceKey is not null ? null :
serviceType.IsInstanceOfType(this) ? this :
serviceType.IsInstanceOfType(server) ? server :
null;
}
/// <inheritdoc/>
void IDisposable.Dispose() { } // nop
}
}