Restructure IMcpEndpoint, IMcpClient, and IMcpServer (#723)
* Remove `McpEndpoint` & `IMcpClient` * Revise naming, add abstract classes * Fix missing renames * Move extension methods to instance methods * Add back old API, but obsoleted * Add back obsoleted factory classes * Move deprecation attributes * `SseClientTransport` -> `HttpClientTransport` * Update src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs Co-authored-by: Stephen Halter <halter73@gmail.com> * PR feedback * PR feedback * Add back dispose lock * Fix docs projects * PR feedback * Undo `docs/*.md` changes --------- Co-authored-by: Stephen Halter <halter73@gmail.com>
This commit is contained in:
parent
fe2ce2bc60
commit
38b4a26998
1 changed files with 5169 additions and 3786 deletions
|
|
@ -1,13 +1,9 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
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;
|
||||
|
||||
|
|
@ -16,13 +12,6 @@ namespace ModelContextProtocol.Server;
|
|||
/// </summary>
|
||||
public static class McpServerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Caches request schemas for elicitation requests based on the type and serializer options.
|
||||
/// </summary>
|
||||
private static readonly ConditionalWeakTable<JsonSerializerOptions, ConcurrentDictionary<Type, ElicitRequestParams.RequestSchema>> s_elicitResultSchemaCache = new();
|
||||
|
||||
private static Dictionary<string, HashSet<string>>? s_elicitAllowedProperties = null;
|
||||
|
||||
/// <summary>
|
||||
/// Requests to sample an LLM via the client using the specified request parameters.
|
||||
/// </summary>
|
||||
|
|
@ -37,19 +26,10 @@ public static class McpServerExtensions
|
|||
/// It allows detailed control over sampling parameters including messages, system prompt, temperature,
|
||||
/// and token limits.
|
||||
/// </remarks>
|
||||
[Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.SampleAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<CreateMessageResult> SampleAsync(
|
||||
this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
ThrowIfSamplingUnsupported(server);
|
||||
|
||||
return server.SendRequestAsync(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
request,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
=> AsServerOrThrow(server).SampleAsync(request, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Requests to sample an LLM via the client using the provided chat messages and options.
|
||||
|
|
@ -66,104 +46,11 @@ public static class McpServerExtensions
|
|||
/// 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.
|
||||
/// </remarks>
|
||||
public static async Task<ChatResponse> SampleAsync(
|
||||
[Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.SampleAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774]
|
||||
public static Task<ChatResponse> SampleAsync(
|
||||
this IMcpServer server,
|
||||
IEnumerable<ChatMessage> messages, ChatOptions? options = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
Throw.IfNull(messages);
|
||||
|
||||
StringBuilder? systemPrompt = null;
|
||||
|
||||
if (options?.Instructions is { } instructions)
|
||||
{
|
||||
(systemPrompt ??= new()).Append(instructions);
|
||||
}
|
||||
|
||||
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 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 server.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,
|
||||
}
|
||||
};
|
||||
}
|
||||
=> AsServerOrThrow(server).SampleAsync(messages, options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="IChatClient"/> wrapper that can be used to send sampling requests to the client.
|
||||
|
|
@ -172,23 +59,16 @@ public static class McpServerExtensions
|
|||
/// <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="InvalidOperationException">The client does not support sampling.</exception>
|
||||
[Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.AsSamplingChatClient)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static IChatClient AsSamplingChatClient(this IMcpServer server)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
ThrowIfSamplingUnsupported(server);
|
||||
|
||||
return new SamplingChatClient(server);
|
||||
}
|
||||
=> AsServerOrThrow(server).AsSamplingChatClient();
|
||||
|
||||
/// <summary>Gets an <see cref="ILogger"/> on which logged messages will be sent as notifications to the client.</summary>
|
||||
/// <param name="server">The server to wrap as an <see cref="ILogger"/>.</param>
|
||||
/// <returns>An <see cref="ILogger"/> that can be used to log to the client..</returns>
|
||||
[Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.AsSamplingChatClient)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ILoggerProvider AsClientLoggerProvider(this IMcpServer server)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
|
||||
return new ClientLoggerProvider(server);
|
||||
}
|
||||
=> AsServerOrThrow(server).AsClientLoggerProvider();
|
||||
|
||||
/// <summary>
|
||||
/// Requests the client to list the roots it exposes.
|
||||
|
|
@ -205,19 +85,10 @@ public static class McpServerExtensions
|
|||
/// 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.
|
||||
/// </remarks>
|
||||
[Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.RequestRootsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<ListRootsResult> RequestRootsAsync(
|
||||
this IMcpServer server, ListRootsRequestParams request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
ThrowIfRootsUnsupported(server);
|
||||
|
||||
return server.SendRequestAsync(
|
||||
RequestMethods.RootsList,
|
||||
request,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
=> AsServerOrThrow(server).RequestRootsAsync(request, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Requests additional information from the user via the client, allowing the server to elicit structured data.
|
||||
|
|
@ -231,327 +102,30 @@ public static class McpServerExtensions
|
|||
/// <remarks>
|
||||
/// This method requires the client to support the elicitation capability.
|
||||
/// </remarks>
|
||||
[Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.ElicitAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<ElicitResult> ElicitAsync(
|
||||
this IMcpServer server, ElicitRequestParams request, CancellationToken cancellationToken = default)
|
||||
=> AsServerOrThrow(server).ElicitAsync(request, cancellationToken);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
private static McpServer AsServerOrThrow(IMcpServer server, [CallerMemberName] string memberName = "")
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
ThrowIfElicitationUnsupported(server);
|
||||
|
||||
return server.SendRequestAsync(
|
||||
RequestMethods.ElicitationCreate,
|
||||
request,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests additional information from the user via the client, constructing a request schema from the
|
||||
/// public serializable properties of <typeparamref name="T"/> and deserializing the response into <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type describing the expected input shape. Only primitive members are supported (string, number, boolean, enum).</typeparam>
|
||||
/// <param name="server">The server initiating the request.</param>
|
||||
/// <param name="message">The message to present to the user.</param>
|
||||
/// <param name="serializerOptions">Serializer options that influence property naming and deserialization.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>An <see cref="ElicitResult{T}"/> with the user's response, if accepted.</returns>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public static async ValueTask<ElicitResult<T>> ElicitAsync<T>(
|
||||
this IMcpServer server,
|
||||
string message,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
ThrowIfElicitationUnsupported(server);
|
||||
|
||||
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
|
||||
if (server is not McpServer mcpServer)
|
||||
{
|
||||
Message = message,
|
||||
RequestedSchema = schema,
|
||||
};
|
||||
|
||||
var raw = await server.ElicitAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!raw.IsAccepted || raw.Content is null)
|
||||
{
|
||||
return new ElicitResult<T> { Action = raw.Action, Content = default };
|
||||
ThrowInvalidSessionType(memberName);
|
||||
}
|
||||
|
||||
var obj = new JsonObject();
|
||||
foreach (var kvp in raw.Content)
|
||||
{
|
||||
obj[kvp.Key] = JsonNode.Parse(kvp.Value.GetRawText());
|
||||
}
|
||||
return mcpServer;
|
||||
|
||||
T? typed = JsonSerializer.Deserialize(obj, serializerOptions.GetTypeInfo<T>());
|
||||
return new ElicitResult<T> { Action = raw.Action, Content = typed };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a request schema for elicitation based on the public serializable properties of <paramref name="type"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the schema being built.</param>
|
||||
/// <param name="serializerOptions">The serializer options to use.</param>
|
||||
/// <returns>The built request schema.</returns>
|
||||
/// <exception cref="McpException"></exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a primitive schema definition for the specified type, if supported.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to create the schema for.</param>
|
||||
/// <param name="serializerOptions">The serializer options to use.</param>
|
||||
/// <returns>The created primitive schema definition.</returns>
|
||||
/// <exception cref="McpException">Thrown when the type is not supported.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.).
|
||||
/// </summary>
|
||||
/// <param name="schema">The schema to validate.</param>
|
||||
/// <param name="type">The type of the schema being validated, just for reporting errors.</param>
|
||||
/// <param name="error">The error message, if validation fails.</param>
|
||||
/// <returns></returns>
|
||||
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 static void ThrowIfSamplingUnsupported(IMcpServer server)
|
||||
{
|
||||
if (server.ClientCapabilities?.Sampling is null)
|
||||
{
|
||||
if (server.ServerOptions.KnownClientInfo is not null)
|
||||
{
|
||||
throw new InvalidOperationException("Sampling is not supported in stateless mode.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Client does not support sampling.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowIfRootsUnsupported(IMcpServer server)
|
||||
{
|
||||
if (server.ClientCapabilities?.Roots is null)
|
||||
{
|
||||
if (server.ServerOptions.KnownClientInfo is not null)
|
||||
{
|
||||
throw new InvalidOperationException("Roots are not supported in stateless mode.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Client does not support roots.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowIfElicitationUnsupported(IMcpServer server)
|
||||
{
|
||||
if (server.ClientCapabilities?.Elicitation is null)
|
||||
{
|
||||
if (server.ServerOptions.KnownClientInfo is not null)
|
||||
{
|
||||
throw new InvalidOperationException("Elicitation is not supported in stateless mode.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Client does not support elicitation requests.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Provides an <see cref="IChatClient"/> implementation that's implemented via client sampling.</summary>
|
||||
private sealed class SamplingChatClient(IMcpServer server) : IChatClient
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
server.SampleAsync(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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides an <see cref="ILoggerProvider"/> implementation for creating loggers
|
||||
/// that send logging message notifications to the client for logged messages.
|
||||
/// </summary>
|
||||
private sealed class ClientLoggerProvider(IMcpServer server) : ILoggerProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
Throw.IfNull(categoryName);
|
||||
|
||||
return new ClientLogger(server, categoryName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IDisposable.Dispose() { }
|
||||
|
||||
private sealed class ClientLogger(IMcpServer server, string categoryName) : ILogger
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull =>
|
||||
null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) =>
|
||||
server?.LoggingLevel is { } loggingLevel &&
|
||||
McpServer.ToLoggingLevel(logLevel) >= loggingLevel;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowInvalidSessionType(string memberName)
|
||||
=> throw new InvalidOperationException(
|
||||
$"Only arguments assignable to '{nameof(McpServer)}' are supported. " +
|
||||
$"Prefer using '{nameof(McpServer)}.{memberName}' instead, as " +
|
||||
$"'{nameof(McpServerExtensions)}.{memberName}' is obsolete and will be " +
|
||||
$"removed in the future.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue