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,
});
}
}
}
}
}