using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization.Metadata;
namespace ModelContextProtocol.Server;
///
/// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client.
///
#pragma warning disable CS0618 // Type or member is obsolete
public abstract partial class McpServer : McpSession, IMcpServer
#pragma warning restore CS0618 // Type or member is obsolete
{
///
/// Caches request schemas for elicitation requests based on the type and serializer options.
///
private static readonly ConditionalWeakTable> s_elicitResultSchemaCache = new();
private static Dictionary>? s_elicitAllowedProperties = null;
///
/// Creates a new instance of an .
///
/// Transport to use for the server representing an already-established MCP session.
/// Configuration options for this server, including capabilities.
/// Logger factory to use for logging. If null, logging will be disabled.
/// Optional service provider to create new instances of tools and other dependencies.
/// An instance that should be disposed when no longer needed.
/// is .
/// is .
public static McpServer Create(
ITransport transport,
McpServerOptions serverOptions,
ILoggerFactory? loggerFactory = null,
IServiceProvider? serviceProvider = null)
{
Throw.IfNull(transport);
Throw.IfNull(serverOptions);
return new McpServerImpl(transport, serverOptions, loggerFactory, serviceProvider);
}
///
/// Requests to sample an LLM via the client using the specified request parameters.
///
/// The parameters for the sampling request.
/// The to monitor for cancellation requests.
/// A task containing the sampling result from the client.
/// The client does not support sampling.
public ValueTask SampleAsync(
CreateMessageRequestParams request, CancellationToken cancellationToken = default)
{
ThrowIfSamplingUnsupported();
return 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 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 .
/// The client does not support sampling.
public async Task SampleAsync(
IEnumerable messages, ChatOptions? options = default, CancellationToken cancellationToken = default)
{
Throw.IfNull(messages);
StringBuilder? systemPrompt = null;
if (options?.Instructions is { } instructions)
{
(systemPrompt ??= new()).Append(instructions);
}
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 TextContentBlock { Text = textContent.Text },
});
break;
case DataContent dataContent when dataContent.HasTopLevelMediaType("image") || dataContent.HasTopLevelMediaType("audio"):
samplingMessages.Add(new()
{
Role = role,
Content = dataContent.HasTopLevelMediaType("image") ?
new ImageContentBlock
{
MimeType = dataContent.MediaType,
Data = dataContent.Base64Data.ToString(),
} :
new AudioContentBlock
{
MimeType = dataContent.MediaType,
Data = dataContent.Base64Data.ToString(),
},
});
break;
}
}
}
}
ModelPreferences? modelPreferences = null;
if (options?.ModelId is { } modelId)
{
modelPreferences = new() { Hints = [new() { Name = modelId }] };
}
var result = await SampleAsync(new()
{
Messages = samplingMessages,
MaxTokens = options?.MaxOutputTokens,
StopSequences = options?.StopSequences?.ToArray(),
SystemPrompt = systemPrompt?.ToString(),
Temperature = options?.Temperature,
ModelPreferences = modelPreferences,
}, cancellationToken).ConfigureAwait(false);
AIContent? responseContent = result.Content.ToAIContent();
return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContent is not null ? [responseContent] : []))
{
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 that can be used to issue sampling requests to the client.
/// The client does not support sampling.
public IChatClient AsSamplingChatClient()
{
ThrowIfSamplingUnsupported();
return new SamplingChatClient(this);
}
/// Gets an on which logged messages will be sent as notifications to the client.
/// An that can be used to log to the client..
public ILoggerProvider AsClientLoggerProvider()
{
return new ClientLoggerProvider(this);
}
///
/// Requests the client to list the roots it exposes.
///
/// The parameters for the list roots request.
/// The to monitor for cancellation requests.
/// A task containing the list of roots exposed by the client.
/// The client does not support roots.
public ValueTask RequestRootsAsync(
ListRootsRequestParams request, CancellationToken cancellationToken = default)
{
ThrowIfRootsUnsupported();
return SendRequestAsync(
RequestMethods.RootsList,
request,
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
McpJsonUtilities.JsonContext.Default.ListRootsResult,
cancellationToken: cancellationToken);
}
///
/// Requests additional information from the user via the client, allowing the server to elicit structured data.
///
/// The parameters for the elicitation request.
/// The to monitor for cancellation requests.
/// A task containing the elicitation result.
/// The client does not support elicitation.
public ValueTask ElicitAsync(
ElicitRequestParams request, CancellationToken cancellationToken = default)
{
ThrowIfElicitationUnsupported();
return SendRequestAsync(
RequestMethods.ElicitationCreate,
request,
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
McpJsonUtilities.JsonContext.Default.ElicitResult,
cancellationToken: cancellationToken);
}
///
/// Requests additional information from the user via the client, constructing a request schema from the
/// public serializable properties of and deserializing the response into .
///
/// The type describing the expected input shape. Only primitive members are supported (string, number, boolean, enum).
/// The message to present to the user.
/// Serializer options that influence property naming and deserialization.
/// The to monitor for cancellation requests.
/// An with the user's response, if accepted.
///
/// Elicitation uses a constrained subset of JSON Schema and only supports strings, numbers/integers, booleans and string enums.
/// Unsupported member types are ignored when constructing the schema.
///
public async ValueTask> ElicitAsync(
string message,
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default)
{
ThrowIfElicitationUnsupported();
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
var dict = s_elicitResultSchemaCache.GetValue(serializerOptions, _ => new());
#if NET
var schema = dict.GetOrAdd(typeof(T), static (t, s) => BuildRequestSchema(t, s), serializerOptions);
#else
var schema = dict.GetOrAdd(typeof(T), type => BuildRequestSchema(type, serializerOptions));
#endif
var request = new ElicitRequestParams
{
Message = message,
RequestedSchema = schema,
};
var raw = await ElicitAsync(request, cancellationToken).ConfigureAwait(false);
if (!raw.IsAccepted || raw.Content is null)
{
return new ElicitResult { Action = raw.Action, Content = default };
}
var obj = new JsonObject();
foreach (var kvp in raw.Content)
{
obj[kvp.Key] = JsonNode.Parse(kvp.Value.GetRawText());
}
T? typed = JsonSerializer.Deserialize(obj, serializerOptions.GetTypeInfo());
return new ElicitResult { Action = raw.Action, Content = typed };
}
///
/// Builds a request schema for elicitation based on the public serializable properties of .
///
/// The type of the schema being built.
/// The serializer options to use.
/// The built request schema.
///
private static ElicitRequestParams.RequestSchema BuildRequestSchema(Type type, JsonSerializerOptions serializerOptions)
{
var schema = new ElicitRequestParams.RequestSchema();
var props = schema.Properties;
JsonTypeInfo typeInfo = serializerOptions.GetTypeInfo(type);
if (typeInfo.Kind != JsonTypeInfoKind.Object)
{
throw new McpException($"Type '{type.FullName}' is not supported for elicitation requests.");
}
foreach (JsonPropertyInfo pi in typeInfo.Properties)
{
var def = CreatePrimitiveSchema(pi.PropertyType, serializerOptions);
props[pi.Name] = def;
}
return schema;
}
///
/// Creates a primitive schema definition for the specified type, if supported.
///
/// The type to create the schema for.
/// The serializer options to use.
/// The created primitive schema definition.
/// Thrown when the type is not supported.
private static ElicitRequestParams.PrimitiveSchemaDefinition CreatePrimitiveSchema(Type type, JsonSerializerOptions serializerOptions)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
throw new McpException($"Type '{type.FullName}' is not a supported property type for elicitation requests. Nullable types are not supported.");
}
var typeInfo = serializerOptions.GetTypeInfo(type);
if (typeInfo.Kind != JsonTypeInfoKind.None)
{
throw new McpException($"Type '{type.FullName}' is not a supported property type for elicitation requests.");
}
var jsonElement = AIJsonUtilities.CreateJsonSchema(type, serializerOptions: serializerOptions);
if (!TryValidateElicitationPrimitiveSchema(jsonElement, type, out var error))
{
throw new McpException(error);
}
var primitiveSchemaDefinition =
jsonElement.Deserialize(McpJsonUtilities.JsonContext.Default.PrimitiveSchemaDefinition);
if (primitiveSchemaDefinition is null)
throw new McpException($"Type '{type.FullName}' is not a supported property type for elicitation requests.");
return primitiveSchemaDefinition;
}
///
/// Validate the produced schema strictly to the subset we support. We only accept an object schema
/// with a supported primitive type keyword and no additional unsupported keywords.Reject things like
/// {}, 'true', or schemas that include unrelated keywords(e.g.items, properties, patternProperties, etc.).
///
/// The schema to validate.
/// The type of the schema being validated, just for reporting errors.
/// The error message, if validation fails.
///
private static bool TryValidateElicitationPrimitiveSchema(JsonElement schema, Type type,
[NotNullWhen(false)] out string? error)
{
if (schema.ValueKind is not JsonValueKind.Object)
{
error = $"Schema generated for type '{type.FullName}' is invalid: expected an object schema.";
return false;
}
if (!schema.TryGetProperty("type", out JsonElement typeProperty)
|| typeProperty.ValueKind is not JsonValueKind.String)
{
error = $"Schema generated for type '{type.FullName}' is invalid: missing or invalid 'type' keyword.";
return false;
}
var typeKeyword = typeProperty.GetString();
if (string.IsNullOrEmpty(typeKeyword))
{
error = $"Schema generated for type '{type.FullName}' is invalid: empty 'type' value.";
return false;
}
if (typeKeyword is not ("string" or "number" or "integer" or "boolean"))
{
error = $"Schema generated for type '{type.FullName}' is invalid: unsupported primitive type '{typeKeyword}'.";
return false;
}
s_elicitAllowedProperties ??= new()
{
["string"] = ["type", "title", "description", "minLength", "maxLength", "format", "enum", "enumNames"],
["number"] = ["type", "title", "description", "minimum", "maximum"],
["integer"] = ["type", "title", "description", "minimum", "maximum"],
["boolean"] = ["type", "title", "description", "default"]
};
var allowed = s_elicitAllowedProperties[typeKeyword];
foreach (JsonProperty prop in schema.EnumerateObject())
{
if (!allowed.Contains(prop.Name))
{
error = $"The property '{type.FullName}.{prop.Name}' is not supported for elicitation.";
return false;
}
}
error = string.Empty;
return true;
}
private void ThrowIfSamplingUnsupported()
{
if (ClientCapabilities?.Sampling is null)
{
if (ServerOptions.KnownClientInfo is not null)
{
throw new InvalidOperationException("Sampling is not supported in stateless mode.");
}
throw new InvalidOperationException("Client does not support sampling.");
}
}
private void ThrowIfRootsUnsupported()
{
if (ClientCapabilities?.Roots is null)
{
if (ServerOptions.KnownClientInfo is not null)
{
throw new InvalidOperationException("Roots are not supported in stateless mode.");
}
throw new InvalidOperationException("Client does not support roots.");
}
}
private void ThrowIfElicitationUnsupported()
{
if (ClientCapabilities?.Elicitation is null)
{
if (ServerOptions.KnownClientInfo is not null)
{
throw new InvalidOperationException("Elicitation is not supported in stateless mode.");
}
throw new InvalidOperationException("Client does not support elicitation requests.");
}
}
/// Provides an implementation that's implemented via client sampling.
private sealed class SamplingChatClient : IChatClient
{
private readonly McpServer _server;
public SamplingChatClient(McpServer server) => _server = server;
///
public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
_server.SampleAsync(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 : ILoggerProvider
{
private readonly McpServer _server;
public ClientLoggerProvider(McpServer server) => _server = server;
///
public ILogger CreateLogger(string categoryName)
{
Throw.IfNull(categoryName);
return new ClientLogger(_server, categoryName);
}
///
void IDisposable.Dispose() { }
private sealed class ClientLogger : ILogger
{
private readonly McpServer _server;
private readonly string _categoryName;
public ClientLogger(McpServer server, string categoryName)
{
_server = server;
_categoryName = categoryName;
}
///
public IDisposable? BeginScope(TState state) where TState : notnull =>
null;
///
public bool IsEnabled(LogLevel logLevel) =>
_server?.LoggingLevel is { } loggingLevel &&
McpServerImpl.ToLoggingLevel(logLevel) >= loggingLevel;
///
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
Throw.IfNull(formatter);
LogInternal(logLevel, formatter(state, exception));
void LogInternal(LogLevel level, string message)
{
_ = _server.SendNotificationAsync(NotificationMethods.LoggingMessageNotification, new LoggingMessageNotificationParams
{
Level = McpServerImpl.ToLoggingLevel(level),
Data = JsonSerializer.SerializeToElement(message, McpJsonUtilities.JsonContext.Default.String),
Logger = _categoryName,
});
}
}
}
}
}