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.
///
public abstract partial class McpServer : McpSession
{
///
/// 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 .
///
/// The 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.
/// or 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.
/// is .
/// The client does not support sampling.
/// The request failed or the client returned an error response.
///
///
/// When the server is using the Streamable HTTP transport, prefer calling this method on the
/// instance available via RequestContext from inside a tool, prompt,
/// or resource handler. That routes the request through the originating POST response stream via
/// , which is always open for the duration of
/// the request, rather than relying on the optional standalone GET SSE stream.
///
///
/// When called during task-augmented tool execution, this method automatically updates the task
/// status to while waiting for the client response,
/// then returns to when the response is received.
///
///
[Obsolete(Obsoletions.DeprecatedSampling_Message, DiagnosticId = Obsoletions.Deprecated_DiagnosticId, UrlFormat = Obsoletions.Deprecated_Url)]
public ValueTask SampleAsync(
CreateMessageRequestParams requestParams,
CancellationToken cancellationToken = default)
{
Throw.IfNull(requestParams);
// If executing inside a background task, redirect sampling through the task store.
// Capability checks (ThrowIfSamplingUnsupported) are intentionally skipped here because the
// client opted into the tasks extension when submitting the originating request, and input
// requests are delivered through the tasks/get response channel rather than as direct
// server->client requests. See SendRequestViaTaskAsync remarks.
if (McpTaskExecutionContext.Current.Value is { } taskContext)
{
return SendRequestViaTaskAsync(taskContext, RequestMethods.SamplingCreateMessage, requestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult,
cancellationToken);
}
ThrowIfSamplingUnsupported();
return SendRequestAsync(
RequestMethods.SamplingCreateMessage,
requestParams,
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 use for serializing user-provided objects. If , is used.
/// 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.
/// The request failed or the client returned an error response.
[Obsolete(Obsoletions.DeprecatedSampling_Message, DiagnosticId = Obsoletions.Deprecated_DiagnosticId, UrlFormat = Obsoletions.Deprecated_Url)]
public async Task SampleAsync(
IEnumerable messages, ChatOptions? chatOptions = default, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default)
{
Throw.IfNull(messages);
serializerOptions ??= McpJsonUtilities.DefaultOptions;
StringBuilder? systemPrompt = null;
if (chatOptions?.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;
}
Role role = message.Role == ChatRole.Assistant ? Role.Assistant : Role.User;
// Group all content blocks from this message into a single SamplingMessage
List contentBlocks = [];
foreach (var content in message.Contents)
{
if (content.ToContentBlock() is { } contentBlock)
{
contentBlocks.Add(contentBlock);
}
}
if (contentBlocks.Count > 0)
{
samplingMessages.Add(new()
{
Role = role,
Content = contentBlocks,
});
}
}
ModelPreferences? modelPreferences = null;
if (chatOptions?.ModelId is { } modelId)
{
modelPreferences = new() { Hints = [new() { Name = modelId }] };
}
IList? tools = null;
if (chatOptions?.Tools is { Count: > 0 })
{
foreach (var tool in chatOptions.Tools)
{
if (tool is AIFunctionDeclaration af)
{
(tools ??= []).Add(new()
{
Name = af.Name,
Description = af.Description,
InputSchema = af.JsonSchema,
Meta = af.AdditionalProperties.ToJsonObject(serializerOptions),
});
}
}
}
ToolChoice? toolChoice = chatOptions?.ToolMode switch
{
NoneChatToolMode => new() { Mode = ToolChoice.ModeNone },
AutoChatToolMode => new() { Mode = ToolChoice.ModeAuto },
RequiredChatToolMode => new() { Mode = ToolChoice.ModeRequired },
_ => null,
};
var result = await SampleAsync(new CreateMessageRequestParams
{
MaxTokens = chatOptions?.MaxOutputTokens ?? ServerOptions.MaxSamplingOutputTokens,
Messages = samplingMessages,
ModelPreferences = modelPreferences,
StopSequences = chatOptions?.StopSequences?.ToArray(),
SystemPrompt = systemPrompt?.ToString(),
Temperature = chatOptions?.Temperature,
ToolChoice = toolChoice,
Tools = tools,
Meta = chatOptions?.AdditionalProperties?.ToJsonObject(serializerOptions),
}, cancellationToken).ConfigureAwait(false);
List responseContents = [];
foreach (var block in result.Content)
{
if (block.ToAIContent(serializerOptions) is { } content)
{
responseContents.Add(content);
}
}
return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContents))
{
CreatedAt = DateTimeOffset.UtcNow,
FinishReason = result.StopReason switch
{
CreateMessageResult.StopReasonEndTurn => ChatFinishReason.Stop,
CreateMessageResult.StopReasonMaxTokens => ChatFinishReason.Length,
CreateMessageResult.StopReasonStopSequence => ChatFinishReason.Stop,
CreateMessageResult.StopReasonToolUse => ChatFinishReason.ToolCalls,
_ => null,
},
ModelId = result.Model,
};
}
///
/// Creates an wrapper that can be used to send sampling requests to the client.
///
/// The to use for serialization. If , is used.
/// The that can be used to issue sampling requests to the client.
/// The client does not support sampling.
///
/// When the server is using the Streamable HTTP transport, prefer obtaining this chat client from the
/// instance available via RequestContext from inside a tool, prompt,
/// or resource handler. That routes sampling requests through the originating POST response stream via
/// , which is always open for the duration of
/// the request, rather than relying on the optional standalone GET SSE stream.
///
[Obsolete(Obsoletions.DeprecatedSampling_Message, DiagnosticId = Obsoletions.Deprecated_DiagnosticId, UrlFormat = Obsoletions.Deprecated_Url)]
public IChatClient AsSamplingChatClient(JsonSerializerOptions? serializerOptions = null)
{
ThrowIfSamplingUnsupported();
return new SamplingChatClient(this, serializerOptions ?? McpJsonUtilities.DefaultOptions);
}
/// Gets an on which logged messages will be sent as notifications to the client.
/// An that can be used to log to the client.
[Obsolete(Obsoletions.DeprecatedLogging_Message, DiagnosticId = Obsoletions.Deprecated_DiagnosticId, UrlFormat = Obsoletions.Deprecated_Url)]
public ILoggerProvider AsClientLoggerProvider() =>
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.
/// is .
/// The client does not support roots.
/// The request failed or the client returned an error response.
///
/// When the server is using the Streamable HTTP transport, prefer calling this method on the
/// instance available via RequestContext from inside a tool, prompt,
/// or resource handler. That routes the request through the originating POST response stream via
/// , which is always open for the duration of
/// the request, rather than relying on the optional standalone GET SSE stream.
///
[Obsolete(Obsoletions.DeprecatedRoots_Message, DiagnosticId = Obsoletions.Deprecated_DiagnosticId, UrlFormat = Obsoletions.Deprecated_Url)]
public ValueTask RequestRootsAsync(
ListRootsRequestParams requestParams,
CancellationToken cancellationToken = default)
{
Throw.IfNull(requestParams);
// If executing inside a background task, redirect through the task store.
// Capability checks (ThrowIfRootsUnsupported) are intentionally skipped here because the
// client opted into the tasks extension when submitting the originating request, and input
// requests are delivered through the tasks/get response channel rather than as direct
// server->client requests. See SendRequestViaTaskAsync remarks.
if (McpTaskExecutionContext.Current.Value is { } taskContext)
{
return SendRequestViaTaskAsync(taskContext, RequestMethods.RootsList, requestParams,
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
McpJsonUtilities.JsonContext.Default.ListRootsResult,
cancellationToken);
}
ThrowIfRootsUnsupported();
return SendRequestAsync(
RequestMethods.RootsList,
requestParams,
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.
/// is .
/// The client does not support elicitation.
/// The request failed or the client returned an error response.
///
///
/// When the server is using the Streamable HTTP transport, prefer calling this method on the
/// instance available via RequestContext from inside a tool, prompt,
/// or resource handler. That routes the request through the originating POST response stream via
/// , which is always open for the duration of
/// the request, rather than relying on the optional standalone GET SSE stream.
///
///
/// When called during task-augmented tool execution, this method automatically updates the task
/// status to while waiting for user input,
/// then returns to when the response is received.
///
///
public async ValueTask ElicitAsync(
ElicitRequestParams requestParams,
CancellationToken cancellationToken = default)
{
Throw.IfNull(requestParams);
// If executing inside a background task, redirect elicitation through the task store.
// Capability checks (ThrowIfElicitationUnsupported) are intentionally skipped here because
// the client opted into the tasks extension when submitting the originating request, and
// input requests are delivered through the tasks/get response channel rather than as
// direct server->client requests. See SendRequestViaTaskAsync remarks.
if (McpTaskExecutionContext.Current.Value is { } taskContext)
{
var taskResult = await SendRequestViaTaskAsync(taskContext, RequestMethods.ElicitationCreate, requestParams,
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
McpJsonUtilities.JsonContext.Default.ElicitResult,
cancellationToken).ConfigureAwait(false);
return taskResult ?? new ElicitResult { Action = "cancel" };
}
ThrowIfElicitationUnsupported(requestParams);
var result = await SendRequestAsync(
RequestMethods.ElicitationCreate,
requestParams,
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
McpJsonUtilities.JsonContext.Default.ElicitResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
return ElicitResult.WithDefaults(requestParams, result);
}
///
/// 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.
/// Optional request options including metadata, serialization settings, and progress tracking.
/// The to monitor for cancellation requests.
/// An with the user's response, if accepted.
/// is .
/// is empty or composed entirely of whitespace.
/// The client does not support elicitation.
/// The request failed or the client returned an error response.
///
/// 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,
RequestOptions? options = null,
CancellationToken cancellationToken = default)
{
Throw.IfNullOrWhiteSpace(message);
var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
var dict = s_elicitResultSchemaCache.GetValue(serializerOptions, _ => new());
var schema = dict.GetOrAdd(typeof(T),
#if NET
static (t, s) => BuildRequestSchema(t, s), serializerOptions);
#else
type => BuildRequestSchema(type, serializerOptions));
#endif
var request = new ElicitRequestParams
{
Message = message,
RequestedSchema = schema,
Meta = options?.GetMetaForRequest(),
};
ThrowIfElicitationUnsupported(request);
var raw = await ElicitAsync(request, cancellationToken).ConfigureAwait(false);
if (!raw.IsAccepted || raw.Content is null)
{
return new ElicitResult { Action = raw.Action, Content = default };
}
JsonObject obj = [];
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 };
}
///
/// Sends a task status notification to the connected client.
///
/// The task status notification parameters to send.
/// The to monitor for cancellation requests. The default is .
/// A task that represents the asynchronous send operation.
/// is .
public Task SendTaskStatusNotificationAsync(
TaskStatusNotificationParams notificationParams,
CancellationToken cancellationToken = default)
{
Throw.IfNull(notificationParams);
return SendNotificationAsync(
NotificationMethods.TaskStatusNotification,
notificationParams,
McpJsonUtilities.JsonContext.Default.TaskStatusNotificationParams,
cancellationToken);
}
///
/// 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 McpProtocolException($"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.
/// The type is not supported.
private static ElicitRequestParams.PrimitiveSchemaDefinition CreatePrimitiveSchema(Type type, JsonSerializerOptions serializerOptions)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
throw new McpProtocolException($"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 McpProtocolException($"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 McpProtocolException(error);
}
return
jsonElement.Deserialize(McpJsonUtilities.JsonContext.Default.PrimitiveSchemaDefinition) ??
throw new McpProtocolException($"Type '{type.FullName}' is not a supported property type for elicitation requests.");
}
///
/// 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 (ClientCapabilities is 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 (ClientCapabilities is null)
{
throw new InvalidOperationException("Roots are not supported in stateless mode.");
}
throw new InvalidOperationException("Client does not support roots.");
}
}
///
/// Creates a scope that redirects server-initiated requests (elicitation, sampling, list roots) through
/// the task store as input requests for the duration of the scope. Use this when executing tool logic
/// in the background as a task, so that any server-to-client requests are surfaced to the client via
/// the task's state instead of direct JSON-RPC messages.
///
/// The task ID in the store.
/// The task store to write input requests to.
/// An that restores the previous context when disposed.
public IDisposable CreateMcpTaskScope(
string taskId,
IMcpTaskStore store)
{
Throw.IfNull(taskId);
Throw.IfNull(store);
var previous = McpTaskExecutionContext.Current.Value;
McpTaskExecutionContext.Current.Value = new McpTaskExecutionContext
{
TaskId = taskId,
Store = store,
};
return new McpTaskExecutionContext.Scope(previous);
}
///
/// Sends a server-initiated request through the task store as an input request, then awaits the response.
///
///
/// When executing inside a task scope, capability negotiation checks (such as
/// , , and
/// ) are intentionally skipped by the callers
/// of this helper. The task channel itself is the negotiated capability: the client opted
/// in to the tasks extension when it submitted the originating request, and is responsible
/// for handling or rejecting the input requests surfaced through tasks/get.
///
private async ValueTask SendRequestViaTaskAsync(
McpTaskExecutionContext taskContext,
string method,
TRequest request,
JsonTypeInfo requestTypeInfo,
JsonTypeInfo responseTypeInfo,
CancellationToken cancellationToken)
{
var requestId = Guid.NewGuid().ToString("N");
var paramsJson = JsonSerializer.SerializeToElement(request, requestTypeInfo);
var inputRequest = new InputRequest
{
Method = method,
Params = paramsJson,
};
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
void handler(InputResponseReceivedEventArgs args)
{
if (args.TaskId == taskContext.TaskId && args.RequestId == requestId)
{
tcs.TrySetResult(args.Response);
}
}
taskContext.Store.InputResponseReceived += handler;
try
{
await taskContext.Store.SetInputRequestsAsync(
taskContext.TaskId,
new Dictionary { [requestId] = inputRequest },
cancellationToken).ConfigureAwait(false);
var response = await tcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
return response.Deserialize(responseTypeInfo)!;
}
finally
{
taskContext.Store.InputResponseReceived -= handler;
}
}
private void ThrowIfElicitationUnsupported(ElicitRequestParams request)
{
if (ClientCapabilities is null)
{
throw new InvalidOperationException("Elicitation is not supported in stateless mode.");
}
var elicitationCapability = ClientCapabilities.Elicitation;
if (elicitationCapability is null)
{
throw new InvalidOperationException("Client does not support elicitation requests.");
}
if (string.Equals(request.Mode, "form", StringComparison.Ordinal))
{
if (request.RequestedSchema is null)
{
throw new ArgumentException("Form mode elicitation requests require a requested schema.");
}
if (elicitationCapability.Form is null)
{
throw new InvalidOperationException("Client does not support form mode elicitation requests.");
}
}
else if (string.Equals(request.Mode, "url", StringComparison.Ordinal))
{
if (request.Url is null)
{
throw new ArgumentException("URL mode elicitation requests require a URL.");
}
if (request.ElicitationId is null)
{
throw new ArgumentException("URL mode elicitation requests require an elicitation ID.");
}
if (elicitationCapability.Url is null)
{
throw new InvalidOperationException("Client does not support URL mode elicitation requests.");
}
}
}
/// Provides an implementation that's implemented via client sampling.
private sealed class SamplingChatClient(McpServer server, JsonSerializerOptions serializerOptions) : IChatClient
{
private readonly McpServer _server = server;
private readonly JsonSerializerOptions _serializerOptions = serializerOptions;
///
public Task GetResponseAsync(IEnumerable messages, ChatOptions? chatOptions = null, CancellationToken cancellationToken = default) =>
_server.SampleAsync(messages, chatOptions, _serializerOptions, cancellationToken);
///
async IAsyncEnumerable IChatClient.GetStreamingResponseAsync(
IEnumerable messages, ChatOptions? chatOptions, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var response = await GetResponseAsync(messages, chatOptions, 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(McpServer server) : ILoggerProvider
{
private readonly McpServer _server = server;
///
public ILogger CreateLogger(string categoryName)
{
Throw.IfNull(categoryName);
return new ClientLogger(_server, categoryName);
}
///
void IDisposable.Dispose() { }
private sealed class ClientLogger(McpServer server, string categoryName) : ILogger
{
private readonly McpServer _server = server;
private readonly string _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,
});
}
}
}
}
}