using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Utils; using ModelContextProtocol.Utils.Json; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; namespace ModelContextProtocol.Server; /// /// Provides extension methods for interacting with an instance. /// public static class McpServerExtensions { /// /// Requests to sample an LLM via the client using the specified request parameters. /// /// The server instance initiating the request. /// The parameters for the sampling request. /// The to monitor for cancellation requests. /// A task containing the sampling result from the client. /// is . /// The client does not support sampling. /// /// This method requires the client to support sampling capabilities. /// It allows detailed control over sampling parameters including messages, system prompt, temperature, /// and token limits. /// public static Task RequestSamplingAsync( this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken) { Throw.IfNull(server); if (server.ClientCapabilities?.Sampling is null) { throw new InvalidOperationException("Client does not support sampling."); } return server.SendRequestAsync( RequestMethods.SamplingCreateMessage, request, McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, McpJsonUtilities.JsonContext.Default.CreateMessageResult, cancellationToken: cancellationToken); } /// /// Requests to sample an LLM via the client using the provided chat messages and options. /// /// The server initiating the request. /// The messages to send as part of the request. /// The options to use for the request, including model parameters and constraints. /// The to monitor for cancellation requests. The default is . /// A task containing the chat response from the model. /// is . /// is . /// The client does not support sampling. /// /// This method converts the provided chat messages into a format suitable for the sampling API, /// handling different content types such as text, images, and audio. /// public static async Task RequestSamplingAsync( this IMcpServer server, IEnumerable messages, ChatOptions? options = default, CancellationToken cancellationToken = default) { Throw.IfNull(server); Throw.IfNull(messages); StringBuilder? systemPrompt = null; List 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") || dataContent.HasTopLevelMediaType("audio"): samplingMessages.Add(new() { Role = role, Content = new() { Type = dataContent.HasTopLevelMediaType("image") ? "image" : "audio", MimeType = dataContent.MediaType, Data = dataContent.GetBase64Data(), }, }); 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); return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, [result.Content.ToAIContent()])) { ModelId = result.Model, FinishReason = result.StopReason switch { "maxTokens" => ChatFinishReason.Length, "endTurn" or "stopSequence" or _ => ChatFinishReason.Stop, } }; } /// /// Creates an wrapper that can be used to send sampling requests to the client. /// /// The server to be wrapped as an . /// The that can be used to issue sampling requests to the client. /// is . /// The client does not support sampling. public static IChatClient AsSamplingChatClient(this IMcpServer server) { Throw.IfNull(server); if (server.ClientCapabilities?.Sampling is null) { throw new InvalidOperationException("Client does not support sampling."); } return new SamplingChatClient(server); } /// Gets an on which logged messages will be sent as notifications to the client. /// The server to wrap as an . /// An that can be used to log to the client.. public static ILoggerProvider AsClientLoggerProvider(this IMcpServer server) { Throw.IfNull(server); return new ClientLoggerProvider(server); } /// /// Requests the client to list the roots it exposes. /// /// The server initiating the request. /// The parameters for the list roots request. /// The to monitor for cancellation requests. /// A task containing the list of roots exposed by the client. /// is . /// The client does not support roots. /// /// This method requires the client to support the roots capability. /// Root resources allow clients to expose a hierarchical structure of resources that can be /// navigated and accessed by the server. These resources might include file systems, databases, /// or other structured data sources that the client makes available through the protocol. /// public static Task RequestRootsAsync( this IMcpServer server, ListRootsRequestParams request, CancellationToken cancellationToken) { Throw.IfNull(server); if (server.ClientCapabilities?.Roots is null) { throw new InvalidOperationException("Client does not support roots."); } return server.SendRequestAsync( RequestMethods.RootsList, request, McpJsonUtilities.JsonContext.Default.ListRootsRequestParams, McpJsonUtilities.JsonContext.Default.ListRootsResult, cancellationToken: cancellationToken); } /// Provides an implementation that's implemented via client sampling. private sealed class SamplingChatClient(IMcpServer server) : IChatClient { /// public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => server.RequestSamplingAsync(messages, options, cancellationToken); /// async IAsyncEnumerable IChatClient.GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken) { var response = await GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); foreach (var update in response.ToChatResponseUpdates()) { yield return update; } } /// 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; } /// void IDisposable.Dispose() { } // nop } /// /// Provides an implementation for creating loggers /// that send logging message notifications to the client for logged messages. /// private sealed class ClientLoggerProvider(IMcpServer server) : ILoggerProvider { /// public ILogger CreateLogger(string categoryName) { Throw.IfNull(categoryName); return new ClientLogger(server, categoryName); } /// void IDisposable.Dispose() { } private sealed class ClientLogger(IMcpServer server, string categoryName) : ILogger { /// public IDisposable? BeginScope(TState state) where TState : notnull => null; /// public bool IsEnabled(LogLevel logLevel) => server?.LoggingLevel is { } loggingLevel && McpServer.ToLoggingLevel(logLevel) >= loggingLevel; /// public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (!IsEnabled(logLevel)) { return; } Throw.IfNull(formatter); Log(logLevel, formatter(state, exception)); void Log(LogLevel logLevel, string message) { _ = server.SendNotificationAsync(NotificationMethods.LoggingMessageNotification, new LoggingMessageNotificationParams() { Level = McpServer.ToLoggingLevel(logLevel), Data = JsonSerializer.SerializeToElement(message, McpJsonUtilities.JsonContext.Default.String), Logger = categoryName, }); } } } } }