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
16 changed files with 5169 additions and 3786 deletions
|
|
@ -3,24 +3,23 @@ using System.Diagnostics;
|
|||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
internal sealed class DestinationBoundMcpServer(McpServer server, ITransport? transport) : IMcpServer
|
||||
internal sealed class DestinationBoundMcpServer(McpServerImpl server, ITransport? transport) : McpServer
|
||||
{
|
||||
public string EndpointName => server.EndpointName;
|
||||
public string? SessionId => transport?.SessionId ?? server.SessionId;
|
||||
public ClientCapabilities? ClientCapabilities => server.ClientCapabilities;
|
||||
public Implementation? ClientInfo => server.ClientInfo;
|
||||
public McpServerOptions ServerOptions => server.ServerOptions;
|
||||
public IServiceProvider? Services => server.Services;
|
||||
public LoggingLevel? LoggingLevel => server.LoggingLevel;
|
||||
public override string? SessionId => transport?.SessionId ?? server.SessionId;
|
||||
public override ClientCapabilities? ClientCapabilities => server.ClientCapabilities;
|
||||
public override Implementation? ClientInfo => server.ClientInfo;
|
||||
public override McpServerOptions ServerOptions => server.ServerOptions;
|
||||
public override IServiceProvider? Services => server.Services;
|
||||
public override LoggingLevel? LoggingLevel => server.LoggingLevel;
|
||||
|
||||
public ValueTask DisposeAsync() => server.DisposeAsync();
|
||||
public override ValueTask DisposeAsync() => server.DisposeAsync();
|
||||
|
||||
public IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler) => server.RegisterNotificationHandler(method, handler);
|
||||
public override IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler) => server.RegisterNotificationHandler(method, handler);
|
||||
|
||||
// This will throw because the server must already be running for this class to be constructed, but it should give us a good Exception message.
|
||||
public Task RunAsync(CancellationToken cancellationToken) => server.RunAsync(cancellationToken);
|
||||
public override Task RunAsync(CancellationToken cancellationToken) => server.RunAsync(cancellationToken);
|
||||
|
||||
public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.Context is not null)
|
||||
{
|
||||
|
|
@ -32,7 +31,7 @@ internal sealed class DestinationBoundMcpServer(McpServer server, ITransport? tr
|
|||
return server.SendMessageAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
|
||||
public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (request.Context is not null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client.
|
||||
/// </summary>
|
||||
[Obsolete($"Use {nameof(McpServer)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public interface IMcpServer : IMcpEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
557
src/ModelContextProtocol.Core/Server/McpServer.Methods.cs
Normal file
557
src/ModelContextProtocol.Core/Server/McpServer.Methods.cs
Normal file
|
|
@ -0,0 +1,557 @@
|
|||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client.
|
||||
/// </summary>
|
||||
#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
|
||||
{
|
||||
/// <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>
|
||||
/// Creates a new instance of an <see cref="McpServer"/>.
|
||||
/// </summary>
|
||||
/// <param name="transport">Transport to use for the server representing an already-established MCP session.</param>
|
||||
/// <param name="serverOptions">Configuration options for this server, including capabilities. </param>
|
||||
/// <param name="loggerFactory">Logger factory to use for logging. If null, logging will be disabled.</param>
|
||||
/// <param name="serviceProvider">Optional service provider to create new instances of tools and other dependencies.</param>
|
||||
/// <returns>An <see cref="McpServer"/> instance that should be disposed when no longer needed.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="transport"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="serverOptions"/> is <see langword="null"/>.</exception>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests to sample an LLM via the client using the specified request parameters.
|
||||
/// </summary>
|
||||
/// <param name="request">The parameters for the sampling request.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task containing the sampling result from the client.</returns>
|
||||
/// <exception cref="InvalidOperationException">The client does not support sampling.</exception>
|
||||
public ValueTask<CreateMessageResult> SampleAsync(
|
||||
CreateMessageRequestParams request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfSamplingUnsupported();
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
request,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests to sample an LLM via the client using the provided chat messages and options.
|
||||
/// </summary>
|
||||
/// <param name="messages">The messages to send as part of the request.</param>
|
||||
/// <param name="options">The options to use for the request, including model parameters and constraints.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task containing the chat response from the model.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The client does not support sampling.</exception>
|
||||
public async Task<ChatResponse> SampleAsync(
|
||||
IEnumerable<ChatMessage> messages, ChatOptions? options = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
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 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,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="IChatClient"/> wrapper that can be used to send sampling requests to the client.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IChatClient"/> that can be used to issue sampling requests to the client.</returns>
|
||||
/// <exception cref="InvalidOperationException">The client does not support sampling.</exception>
|
||||
public IChatClient AsSamplingChatClient()
|
||||
{
|
||||
ThrowIfSamplingUnsupported();
|
||||
return new SamplingChatClient(this);
|
||||
}
|
||||
|
||||
/// <summary>Gets an <see cref="ILogger"/> on which logged messages will be sent as notifications to the client.</summary>
|
||||
/// <returns>An <see cref="ILogger"/> that can be used to log to the client..</returns>
|
||||
public ILoggerProvider AsClientLoggerProvider()
|
||||
{
|
||||
return new ClientLoggerProvider(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests the client to list the roots it exposes.
|
||||
/// </summary>
|
||||
/// <param name="request">The parameters for the list roots request.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task containing the list of roots exposed by the client.</returns>
|
||||
/// <exception cref="InvalidOperationException">The client does not support roots.</exception>
|
||||
public ValueTask<ListRootsResult> RequestRootsAsync(
|
||||
ListRootsRequestParams request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfRootsUnsupported();
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.RootsList,
|
||||
request,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests additional information from the user via the client, allowing the server to elicit structured data.
|
||||
/// </summary>
|
||||
/// <param name="request">The parameters for the elicitation request.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task containing the elicitation result.</returns>
|
||||
/// <exception cref="InvalidOperationException">The client does not support elicitation.</exception>
|
||||
public ValueTask<ElicitResult> ElicitAsync(
|
||||
ElicitRequestParams request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfElicitationUnsupported();
|
||||
|
||||
return 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="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 async ValueTask<ElicitResult<T>> ElicitAsync<T>(
|
||||
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<T> { 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<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 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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Provides an <see cref="IChatClient"/> implementation that's implemented via client sampling.</summary>
|
||||
private sealed class SamplingChatClient : IChatClient
|
||||
{
|
||||
private readonly McpServer _server;
|
||||
|
||||
public SamplingChatClient(McpServer server) => _server = server;
|
||||
|
||||
/// <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 : ILoggerProvider
|
||||
{
|
||||
private readonly McpServer _server;
|
||||
|
||||
public ClientLoggerProvider(McpServer server) => _server = server;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
Throw.IfNull(categoryName);
|
||||
|
||||
return new ClientLogger(_server, categoryName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull =>
|
||||
null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) =>
|
||||
_server?.LoggingLevel is { } loggingLevel &&
|
||||
McpServerImpl.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);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,714 +1,64 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <inheritdoc />
|
||||
internal sealed partial class McpServer : McpEndpoint, IMcpServer
|
||||
/// <summary>
|
||||
/// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client.
|
||||
/// </summary>
|
||||
#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
|
||||
{
|
||||
internal static Implementation DefaultImplementation { get; } = new()
|
||||
{
|
||||
Name = DefaultAssemblyName.Name ?? nameof(McpServer),
|
||||
Version = DefaultAssemblyName.Version?.ToString() ?? "1.0.0",
|
||||
};
|
||||
|
||||
private readonly ITransport _sessionTransport;
|
||||
private readonly bool _servicesScopePerRequest;
|
||||
private readonly List<Action> _disposables = [];
|
||||
|
||||
private readonly string _serverOnlyEndpointName;
|
||||
private string? _endpointName;
|
||||
private int _started;
|
||||
|
||||
/// <summary>Holds a boxed <see cref="LoggingLevel"/> value for the server.</summary>
|
||||
/// <summary>
|
||||
/// Gets the capabilities supported by the client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initialized to non-null the first time SetLevel is used. This is stored as a strong box
|
||||
/// rather than a nullable to be able to manipulate it atomically.
|
||||
/// <para>
|
||||
/// These capabilities are established during the initialization handshake and indicate
|
||||
/// which features the client supports, such as sampling, roots, and other
|
||||
/// protocol-specific functionality.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Server implementations can check these capabilities to determine which features
|
||||
/// are available when interacting with the client.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private StrongBox<LoggingLevel>? _loggingLevel;
|
||||
public abstract ClientCapabilities? ClientCapabilities { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="McpServer"/>.
|
||||
/// Gets the version and implementation information of the connected client.
|
||||
/// </summary>
|
||||
/// <param name="transport">Transport to use for the server representing an already-established session.</param>
|
||||
/// <param name="options">Configuration options for this server, including capabilities.
|
||||
/// Make sure to accurately reflect exactly what capabilities the server supports and does not support.</param>
|
||||
/// <param name="loggerFactory">Logger factory to use for logging</param>
|
||||
/// <param name="serviceProvider">Optional service provider to use for dependency injection</param>
|
||||
/// <exception cref="McpException">The server was incorrectly configured.</exception>
|
||||
public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? loggerFactory, IServiceProvider? serviceProvider)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
Throw.IfNull(transport);
|
||||
Throw.IfNull(options);
|
||||
|
||||
options ??= new();
|
||||
|
||||
_sessionTransport = transport;
|
||||
ServerOptions = options;
|
||||
Services = serviceProvider;
|
||||
_serverOnlyEndpointName = $"Server ({options.ServerInfo?.Name ?? DefaultImplementation.Name} {options.ServerInfo?.Version ?? DefaultImplementation.Version})";
|
||||
_servicesScopePerRequest = options.ScopeRequests;
|
||||
|
||||
ClientInfo = options.KnownClientInfo;
|
||||
UpdateEndpointNameWithClientInfo();
|
||||
|
||||
// Configure all request handlers based on the supplied options.
|
||||
ServerCapabilities = new();
|
||||
ConfigureInitialize(options);
|
||||
ConfigureTools(options);
|
||||
ConfigurePrompts(options);
|
||||
ConfigureResources(options);
|
||||
ConfigureLogging(options);
|
||||
ConfigureCompletion(options);
|
||||
ConfigureExperimental(options);
|
||||
ConfigurePing();
|
||||
|
||||
// Register any notification handlers that were provided.
|
||||
if (options.Capabilities?.NotificationHandlers is { } notificationHandlers)
|
||||
{
|
||||
NotificationHandlers.RegisterRange(notificationHandlers);
|
||||
}
|
||||
|
||||
// Now that everything has been configured, subscribe to any necessary notifications.
|
||||
if (transport is not StreamableHttpServerTransport streamableHttpTransport || streamableHttpTransport.Stateless is false)
|
||||
{
|
||||
Register(ServerOptions.Capabilities?.Tools?.ToolCollection, NotificationMethods.ToolListChangedNotification);
|
||||
Register(ServerOptions.Capabilities?.Prompts?.PromptCollection, NotificationMethods.PromptListChangedNotification);
|
||||
Register(ServerOptions.Capabilities?.Resources?.ResourceCollection, NotificationMethods.ResourceListChangedNotification);
|
||||
|
||||
void Register<TPrimitive>(McpServerPrimitiveCollection<TPrimitive>? collection, string notificationMethod)
|
||||
where TPrimitive : IMcpServerPrimitive
|
||||
{
|
||||
if (collection is not null)
|
||||
{
|
||||
EventHandler changed = (sender, e) => _ = this.SendNotificationAsync(notificationMethod);
|
||||
collection.Changed += changed;
|
||||
_disposables.Add(() => collection.Changed -= changed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And initialize the session.
|
||||
InitializeSession(transport);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? SessionId => _sessionTransport.SessionId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ServerCapabilities ServerCapabilities { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ClientCapabilities? ClientCapabilities { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Implementation? ClientInfo { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public McpServerOptions ServerOptions { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IServiceProvider? Services { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string EndpointName => _endpointName ?? _serverOnlyEndpointName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LoggingLevel? LoggingLevel => _loggingLevel?.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RunAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _started, 1) != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(RunAsync)} must only be called once.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StartSession(_sessionTransport, fullSessionCancellationToken: cancellationToken);
|
||||
await MessageProcessingTask.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeUnsynchronizedAsync()
|
||||
{
|
||||
_disposables.ForEach(d => d());
|
||||
await base.DisposeUnsynchronizedAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void ConfigurePing()
|
||||
{
|
||||
SetHandler(RequestMethods.Ping,
|
||||
async (request, _) => new PingResult(),
|
||||
McpJsonUtilities.JsonContext.Default.JsonNode,
|
||||
McpJsonUtilities.JsonContext.Default.PingResult);
|
||||
}
|
||||
|
||||
private void ConfigureInitialize(McpServerOptions options)
|
||||
{
|
||||
RequestHandlers.Set(RequestMethods.Initialize,
|
||||
async (request, _, _) =>
|
||||
{
|
||||
ClientCapabilities = request?.Capabilities ?? new();
|
||||
ClientInfo = request?.ClientInfo;
|
||||
|
||||
// Use the ClientInfo to update the session EndpointName for logging.
|
||||
UpdateEndpointNameWithClientInfo();
|
||||
GetSessionOrThrow().EndpointName = EndpointName;
|
||||
|
||||
// Negotiate a protocol version. If the server options provide one, use that.
|
||||
// Otherwise, try to use whatever the client requested as long as it's supported.
|
||||
// If it's not supported, fall back to the latest supported version.
|
||||
string? protocolVersion = options.ProtocolVersion;
|
||||
if (protocolVersion is null)
|
||||
{
|
||||
protocolVersion = request?.ProtocolVersion is string clientProtocolVersion && McpSession.SupportedProtocolVersions.Contains(clientProtocolVersion) ?
|
||||
clientProtocolVersion :
|
||||
McpSession.LatestProtocolVersion;
|
||||
}
|
||||
|
||||
return new InitializeResult
|
||||
{
|
||||
ProtocolVersion = protocolVersion,
|
||||
Instructions = options.ServerInstructions,
|
||||
ServerInfo = options.ServerInfo ?? DefaultImplementation,
|
||||
Capabilities = ServerCapabilities ?? new(),
|
||||
};
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.InitializeResult);
|
||||
}
|
||||
|
||||
private void ConfigureCompletion(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Completions is not { } completionsCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var completeHandler = completionsCapability.CompleteHandler ?? (static async (_, __) => new CompleteResult());
|
||||
completeHandler = BuildFilterPipeline(completeHandler, options.Filters.CompleteFilters);
|
||||
|
||||
ServerCapabilities.Completions = new()
|
||||
{
|
||||
CompleteHandler = completeHandler
|
||||
};
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.CompletionComplete,
|
||||
ServerCapabilities.Completions.CompleteHandler,
|
||||
McpJsonUtilities.JsonContext.Default.CompleteRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CompleteResult);
|
||||
}
|
||||
|
||||
private void ConfigureExperimental(McpServerOptions options)
|
||||
{
|
||||
ServerCapabilities.Experimental = options.Capabilities?.Experimental;
|
||||
}
|
||||
|
||||
private void ConfigureResources(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Resources is not { } resourcesCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCapabilities.Resources = new();
|
||||
|
||||
var listResourcesHandler = resourcesCapability.ListResourcesHandler ?? (static async (_, __) => new ListResourcesResult());
|
||||
var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler ?? (static async (_, __) => new ListResourceTemplatesResult());
|
||||
var readResourceHandler = resourcesCapability.ReadResourceHandler ?? (static async (request, _) => throw new McpException($"Unknown resource URI: '{request.Params?.Uri}'", McpErrorCode.InvalidParams));
|
||||
var subscribeHandler = resourcesCapability.SubscribeToResourcesHandler ?? (static async (_, __) => new EmptyResult());
|
||||
var unsubscribeHandler = resourcesCapability.UnsubscribeFromResourcesHandler ?? (static async (_, __) => new EmptyResult());
|
||||
var resources = resourcesCapability.ResourceCollection;
|
||||
var listChanged = resourcesCapability.ListChanged;
|
||||
var subscribe = resourcesCapability.Subscribe;
|
||||
|
||||
// Handle resources provided via DI.
|
||||
if (resources is { IsEmpty: false })
|
||||
{
|
||||
var originalListResourcesHandler = listResourcesHandler;
|
||||
listResourcesHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListResourcesResult result = originalListResourcesHandler is not null ?
|
||||
await originalListResourcesHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
foreach (var r in resources)
|
||||
{
|
||||
if (r.ProtocolResource is { } resource)
|
||||
{
|
||||
result.Resources.Add(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var originalListResourceTemplatesHandler = listResourceTemplatesHandler;
|
||||
listResourceTemplatesHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListResourceTemplatesResult result = originalListResourceTemplatesHandler is not null ?
|
||||
await originalListResourceTemplatesHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
foreach (var rt in resources)
|
||||
{
|
||||
if (rt.IsTemplated)
|
||||
{
|
||||
result.ResourceTemplates.Add(rt.ProtocolResourceTemplate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Synthesize read resource handler, which covers both resources and resource templates.
|
||||
var originalReadResourceHandler = readResourceHandler;
|
||||
readResourceHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
if (request.MatchedPrimitive is McpServerResource matchedResource)
|
||||
{
|
||||
if (await matchedResource.ReadAsync(request, cancellationToken).ConfigureAwait(false) is { } result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return await originalReadResourceHandler(request, cancellationToken).ConfigureAwait(false);
|
||||
};
|
||||
|
||||
listChanged = true;
|
||||
|
||||
// TODO: Implement subscribe/unsubscribe logic for resource and resource template collections.
|
||||
// subscribe = true;
|
||||
}
|
||||
|
||||
listResourcesHandler = BuildFilterPipeline(listResourcesHandler, options.Filters.ListResourcesFilters);
|
||||
listResourceTemplatesHandler = BuildFilterPipeline(listResourceTemplatesHandler, options.Filters.ListResourceTemplatesFilters);
|
||||
readResourceHandler = BuildFilterPipeline(readResourceHandler, options.Filters.ReadResourceFilters, handler =>
|
||||
async (request, cancellationToken) =>
|
||||
{
|
||||
// Initial handler that sets MatchedPrimitive
|
||||
if (request.Params?.Uri is { } uri && resources is not null)
|
||||
{
|
||||
// First try an O(1) lookup by exact match.
|
||||
if (resources.TryGetPrimitive(uri, out var resource))
|
||||
{
|
||||
request.MatchedPrimitive = resource;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back to an O(N) lookup, trying to match against each URI template.
|
||||
// The number of templates is controlled by the server developer, and the number is expected to be
|
||||
// not terribly large. If that changes, this can be tweaked to enable a more efficient lookup.
|
||||
foreach (var resourceTemplate in resources)
|
||||
{
|
||||
// Check if this template would handle the request by testing if ReadAsync would succeed
|
||||
if (resourceTemplate.IsTemplated)
|
||||
{
|
||||
// This is a simplified check - a more robust implementation would match the URI pattern
|
||||
// For now, we'll let the actual handler attempt the match
|
||||
request.MatchedPrimitive = resourceTemplate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await handler(request, cancellationToken).ConfigureAwait(false);
|
||||
});
|
||||
subscribeHandler = BuildFilterPipeline(subscribeHandler, options.Filters.SubscribeToResourcesFilters);
|
||||
unsubscribeHandler = BuildFilterPipeline(unsubscribeHandler, options.Filters.UnsubscribeFromResourcesFilters);
|
||||
|
||||
ServerCapabilities.Resources.ListResourcesHandler = listResourcesHandler;
|
||||
ServerCapabilities.Resources.ListResourceTemplatesHandler = listResourceTemplatesHandler;
|
||||
ServerCapabilities.Resources.ReadResourceHandler = readResourceHandler;
|
||||
ServerCapabilities.Resources.ResourceCollection = resources;
|
||||
ServerCapabilities.Resources.SubscribeToResourcesHandler = subscribeHandler;
|
||||
ServerCapabilities.Resources.UnsubscribeFromResourcesHandler = unsubscribeHandler;
|
||||
ServerCapabilities.Resources.ListChanged = listChanged;
|
||||
ServerCapabilities.Resources.Subscribe = subscribe;
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesList,
|
||||
listResourcesHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesTemplatesList,
|
||||
listResourceTemplatesHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesRead,
|
||||
readResourceHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesSubscribe,
|
||||
subscribeHandler,
|
||||
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesUnsubscribe,
|
||||
unsubscribeHandler,
|
||||
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
}
|
||||
|
||||
private void ConfigurePrompts(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Prompts is not { } promptsCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCapabilities.Prompts = new();
|
||||
|
||||
var listPromptsHandler = promptsCapability.ListPromptsHandler ?? (static async (_, __) => new ListPromptsResult());
|
||||
var getPromptHandler = promptsCapability.GetPromptHandler ?? (static async (request, _) => throw new McpException($"Unknown prompt: '{request.Params?.Name}'", McpErrorCode.InvalidParams));
|
||||
var prompts = promptsCapability.PromptCollection;
|
||||
var listChanged = promptsCapability.ListChanged;
|
||||
|
||||
// Handle tools provided via DI by augmenting the handlers to incorporate them.
|
||||
if (prompts is { IsEmpty: false })
|
||||
{
|
||||
var originalListPromptsHandler = listPromptsHandler;
|
||||
listPromptsHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListPromptsResult result = originalListPromptsHandler is not null ?
|
||||
await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
foreach (var p in prompts)
|
||||
{
|
||||
result.Prompts.Add(p.ProtocolPrompt);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var originalGetPromptHandler = getPromptHandler;
|
||||
getPromptHandler = (request, cancellationToken) =>
|
||||
{
|
||||
if (request.MatchedPrimitive is McpServerPrompt prompt)
|
||||
{
|
||||
return prompt.GetAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
return originalGetPromptHandler(request, cancellationToken);
|
||||
};
|
||||
|
||||
listChanged = true;
|
||||
}
|
||||
|
||||
listPromptsHandler = BuildFilterPipeline(listPromptsHandler, options.Filters.ListPromptsFilters);
|
||||
getPromptHandler = BuildFilterPipeline(getPromptHandler, options.Filters.GetPromptFilters, handler =>
|
||||
(request, cancellationToken) =>
|
||||
{
|
||||
// Initial handler that sets MatchedPrimitive
|
||||
if (request.Params?.Name is { } promptName && prompts is not null &&
|
||||
prompts.TryGetPrimitive(promptName, out var prompt))
|
||||
{
|
||||
request.MatchedPrimitive = prompt;
|
||||
}
|
||||
|
||||
return handler(request, cancellationToken);
|
||||
});
|
||||
|
||||
ServerCapabilities.Prompts.ListPromptsHandler = listPromptsHandler;
|
||||
ServerCapabilities.Prompts.GetPromptHandler = getPromptHandler;
|
||||
ServerCapabilities.Prompts.PromptCollection = prompts;
|
||||
ServerCapabilities.Prompts.ListChanged = listChanged;
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.PromptsList,
|
||||
listPromptsHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.PromptsGet,
|
||||
getPromptHandler,
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptResult);
|
||||
}
|
||||
|
||||
private void ConfigureTools(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Tools is not { } toolsCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCapabilities.Tools = new();
|
||||
|
||||
var listToolsHandler = toolsCapability.ListToolsHandler ?? (static async (_, __) => new ListToolsResult());
|
||||
var callToolHandler = toolsCapability.CallToolHandler ?? (static async (request, _) => throw new McpException($"Unknown tool: '{request.Params?.Name}'", McpErrorCode.InvalidParams));
|
||||
var tools = toolsCapability.ToolCollection;
|
||||
var listChanged = toolsCapability.ListChanged;
|
||||
|
||||
// Handle tools provided via DI by augmenting the handlers to incorporate them.
|
||||
if (tools is { IsEmpty: false })
|
||||
{
|
||||
var originalListToolsHandler = listToolsHandler;
|
||||
listToolsHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListToolsResult result = originalListToolsHandler is not null ?
|
||||
await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
foreach (var t in tools)
|
||||
{
|
||||
result.Tools.Add(t.ProtocolTool);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var originalCallToolHandler = callToolHandler;
|
||||
callToolHandler = (request, cancellationToken) =>
|
||||
{
|
||||
if (request.MatchedPrimitive is McpServerTool tool)
|
||||
{
|
||||
return tool.InvokeAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
return originalCallToolHandler(request, cancellationToken);
|
||||
};
|
||||
|
||||
listChanged = true;
|
||||
}
|
||||
|
||||
listToolsHandler = BuildFilterPipeline(listToolsHandler, options.Filters.ListToolsFilters);
|
||||
callToolHandler = BuildFilterPipeline(callToolHandler, options.Filters.CallToolFilters, handler =>
|
||||
(request, cancellationToken) =>
|
||||
{
|
||||
// Initial handler that sets MatchedPrimitive
|
||||
if (request.Params?.Name is { } toolName && tools is not null &&
|
||||
tools.TryGetPrimitive(toolName, out var tool))
|
||||
{
|
||||
request.MatchedPrimitive = tool;
|
||||
}
|
||||
|
||||
return handler(request, cancellationToken);
|
||||
}, handler =>
|
||||
async (request, cancellationToken) =>
|
||||
{
|
||||
// Final handler that provides exception handling only for tool execution
|
||||
// Only wrap tool execution in try-catch, not tool resolution
|
||||
if (request.MatchedPrimitive is McpServerTool)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await handler(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e) when (e is not OperationCanceledException)
|
||||
{
|
||||
ToolCallError(request.Params?.Name ?? string.Empty, e);
|
||||
|
||||
string errorMessage = e is McpException ?
|
||||
$"An error occurred invoking '{request.Params?.Name}': {e.Message}" :
|
||||
$"An error occurred invoking '{request.Params?.Name}'.";
|
||||
|
||||
return new()
|
||||
{
|
||||
IsError = true,
|
||||
Content = [new TextContentBlock { Text = errorMessage }],
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For unmatched tools, let exceptions bubble up as protocol errors
|
||||
return await handler(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
|
||||
ServerCapabilities.Tools.ListToolsHandler = listToolsHandler;
|
||||
ServerCapabilities.Tools.CallToolHandler = callToolHandler;
|
||||
ServerCapabilities.Tools.ToolCollection = tools;
|
||||
ServerCapabilities.Tools.ListChanged = listChanged;
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ToolsList,
|
||||
listToolsHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ToolsCall,
|
||||
callToolHandler,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolResult);
|
||||
}
|
||||
|
||||
private void ConfigureLogging(McpServerOptions options)
|
||||
{
|
||||
// We don't require that the handler be provided, as we always store the provided log level to the server.
|
||||
var setLoggingLevelHandler = options.Capabilities?.Logging?.SetLoggingLevelHandler;
|
||||
|
||||
// Apply filters to the handler
|
||||
if (setLoggingLevelHandler is not null)
|
||||
{
|
||||
setLoggingLevelHandler = BuildFilterPipeline(setLoggingLevelHandler, options.Filters.SetLoggingLevelFilters);
|
||||
}
|
||||
|
||||
ServerCapabilities.Logging = new();
|
||||
ServerCapabilities.Logging.SetLoggingLevelHandler = setLoggingLevelHandler;
|
||||
|
||||
RequestHandlers.Set(
|
||||
RequestMethods.LoggingSetLevel,
|
||||
(request, jsonRpcRequest, cancellationToken) =>
|
||||
{
|
||||
// Store the provided level.
|
||||
if (request is not null)
|
||||
{
|
||||
if (_loggingLevel is null)
|
||||
{
|
||||
Interlocked.CompareExchange(ref _loggingLevel, new(request.Level), null);
|
||||
}
|
||||
|
||||
_loggingLevel.Value = request.Level;
|
||||
}
|
||||
|
||||
// If a handler was provided, now delegate to it.
|
||||
if (setLoggingLevelHandler is not null)
|
||||
{
|
||||
return InvokeHandlerAsync(setLoggingLevelHandler, request, jsonRpcRequest, cancellationToken);
|
||||
}
|
||||
|
||||
// Otherwise, consider it handled.
|
||||
return new ValueTask<EmptyResult>(EmptyResult.Instance);
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
}
|
||||
|
||||
private ValueTask<TResult> InvokeHandlerAsync<TParams, TResult>(
|
||||
McpRequestHandler<TParams, TResult> handler,
|
||||
TParams? args,
|
||||
JsonRpcRequest jsonRpcRequest,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _servicesScopePerRequest ?
|
||||
InvokeScopedAsync(handler, args, jsonRpcRequest, cancellationToken) :
|
||||
handler(new(new DestinationBoundMcpServer(this, jsonRpcRequest.Context?.RelatedTransport), jsonRpcRequest) { Params = args }, cancellationToken);
|
||||
|
||||
async ValueTask<TResult> InvokeScopedAsync(
|
||||
McpRequestHandler<TParams, TResult> handler,
|
||||
TParams? args,
|
||||
JsonRpcRequest jsonRpcRequest,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scope = Services?.GetService<IServiceScopeFactory>()?.CreateAsyncScope();
|
||||
try
|
||||
{
|
||||
return await handler(
|
||||
new RequestContext<TParams>(new DestinationBoundMcpServer(this, jsonRpcRequest.Context?.RelatedTransport), jsonRpcRequest)
|
||||
{
|
||||
Services = scope?.ServiceProvider ?? Services,
|
||||
Params = args
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (scope is not null)
|
||||
{
|
||||
await scope.Value.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetHandler<TParams, TResult>(
|
||||
string method,
|
||||
McpRequestHandler<TParams, TResult> handler,
|
||||
JsonTypeInfo<TParams> requestTypeInfo,
|
||||
JsonTypeInfo<TResult> responseTypeInfo)
|
||||
{
|
||||
RequestHandlers.Set(method,
|
||||
(request, jsonRpcRequest, cancellationToken) =>
|
||||
InvokeHandlerAsync(handler, request, jsonRpcRequest, cancellationToken),
|
||||
requestTypeInfo, responseTypeInfo);
|
||||
}
|
||||
|
||||
private static McpRequestHandler<TParams, TResult> BuildFilterPipeline<TParams, TResult>(
|
||||
McpRequestHandler<TParams, TResult> baseHandler,
|
||||
List<McpRequestFilter<TParams, TResult>> filters,
|
||||
McpRequestFilter<TParams, TResult>? initialHandler = null,
|
||||
McpRequestFilter<TParams, TResult>? finalHandler = null)
|
||||
{
|
||||
var current = baseHandler;
|
||||
|
||||
if (finalHandler is not null)
|
||||
{
|
||||
current = finalHandler(current);
|
||||
}
|
||||
|
||||
for (int i = filters.Count - 1; i >= 0; i--)
|
||||
{
|
||||
current = filters[i](current);
|
||||
}
|
||||
|
||||
if (initialHandler is not null)
|
||||
{
|
||||
current = initialHandler(current);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private void UpdateEndpointNameWithClientInfo()
|
||||
{
|
||||
if (ClientInfo is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_endpointName = $"{_serverOnlyEndpointName}, Client ({ClientInfo.Name} {ClientInfo.Version})";
|
||||
}
|
||||
|
||||
/// <summary>Maps a <see cref="LogLevel"/> to a <see cref="LoggingLevel"/>.</summary>
|
||||
internal static LoggingLevel ToLoggingLevel(LogLevel level) =>
|
||||
level switch
|
||||
{
|
||||
LogLevel.Trace => Protocol.LoggingLevel.Debug,
|
||||
LogLevel.Debug => Protocol.LoggingLevel.Debug,
|
||||
LogLevel.Information => Protocol.LoggingLevel.Info,
|
||||
LogLevel.Warning => Protocol.LoggingLevel.Warning,
|
||||
LogLevel.Error => Protocol.LoggingLevel.Error,
|
||||
LogLevel.Critical => Protocol.LoggingLevel.Critical,
|
||||
_ => Protocol.LoggingLevel.Emergency,
|
||||
};
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "\"{ToolName}\" threw an unhandled exception.")]
|
||||
private partial void ToolCallError(string toolName, Exception exception);
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property contains identification information about the client that has connected to this server,
|
||||
/// including its name and version. This information is provided by the client during initialization.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Server implementations can use this information for logging, tracking client versions,
|
||||
/// or implementing client-specific behaviors.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract Implementation? ClientInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the options used to construct this server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These options define the server's capabilities, protocol version, and other configuration
|
||||
/// settings that were used to initialize the server.
|
||||
/// </remarks>
|
||||
public abstract McpServerOptions ServerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service provider for the server.
|
||||
/// </summary>
|
||||
public abstract IServiceProvider? Services { get; }
|
||||
|
||||
/// <summary>Gets the last logging level set by the client, or <see langword="null"/> if it's never been set.</summary>
|
||||
public abstract LoggingLevel? LoggingLevel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Runs the server, listening for and handling client requests.
|
||||
/// </summary>
|
||||
public abstract Task RunAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
|
@ -10,6 +10,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// This is the recommended way to create <see cref="IMcpServer"/> instances.
|
||||
/// The factory handles proper initialization of server instances with the required dependencies.
|
||||
/// </remarks>
|
||||
[Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.Create)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static class McpServerFactory
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -27,10 +28,5 @@ public static class McpServerFactory
|
|||
McpServerOptions serverOptions,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
IServiceProvider? serviceProvider = null)
|
||||
{
|
||||
Throw.IfNull(transport);
|
||||
Throw.IfNull(serverOptions);
|
||||
|
||||
return new McpServer(transport, serverOptions, loggerFactory, serviceProvider);
|
||||
}
|
||||
=> McpServer.Create(transport, serverOptions, loggerFactory, serviceProvider);
|
||||
}
|
||||
|
|
|
|||
750
src/ModelContextProtocol.Core/Server/McpServerImpl.cs
Normal file
750
src/ModelContextProtocol.Core/Server/McpServerImpl.cs
Normal file
|
|
@ -0,0 +1,750 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
||||
// TODO: Fix merge conflicts in this file.
|
||||
|
||||
/// <inheritdoc />
|
||||
internal sealed partial class McpServerImpl : McpServer
|
||||
{
|
||||
internal static Implementation DefaultImplementation { get; } = new()
|
||||
{
|
||||
Name = AssemblyNameHelper.DefaultAssemblyName.Name ?? nameof(McpServer),
|
||||
Version = AssemblyNameHelper.DefaultAssemblyName.Version?.ToString() ?? "1.0.0",
|
||||
};
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly ITransport _sessionTransport;
|
||||
private readonly bool _servicesScopePerRequest;
|
||||
private readonly List<Action> _disposables = [];
|
||||
private readonly NotificationHandlers _notificationHandlers;
|
||||
private readonly RequestHandlers _requestHandlers;
|
||||
private readonly McpSessionHandler _sessionHandler;
|
||||
private readonly SemaphoreSlim _disposeLock = new(1, 1);
|
||||
|
||||
private ClientCapabilities? _clientCapabilities;
|
||||
private Implementation? _clientInfo;
|
||||
|
||||
private readonly string _serverOnlyEndpointName;
|
||||
private string _endpointName;
|
||||
private int _started;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>Holds a boxed <see cref="LoggingLevel"/> value for the server.</summary>
|
||||
/// <remarks>
|
||||
/// Initialized to non-null the first time SetLevel is used. This is stored as a strong box
|
||||
/// rather than a nullable to be able to manipulate it atomically.
|
||||
/// </remarks>
|
||||
private StrongBox<LoggingLevel>? _loggingLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="McpServerImpl"/>.
|
||||
/// </summary>
|
||||
/// <param name="transport">Transport to use for the server representing an already-established session.</param>
|
||||
/// <param name="options">Configuration options for this server, including capabilities.
|
||||
/// Make sure to accurately reflect exactly what capabilities the server supports and does not support.</param>
|
||||
/// <param name="loggerFactory">Logger factory to use for logging</param>
|
||||
/// <param name="serviceProvider">Optional service provider to use for dependency injection</param>
|
||||
/// <exception cref="McpException">The server was incorrectly configured.</exception>
|
||||
public McpServerImpl(ITransport transport, McpServerOptions options, ILoggerFactory? loggerFactory, IServiceProvider? serviceProvider)
|
||||
{
|
||||
Throw.IfNull(transport);
|
||||
Throw.IfNull(options);
|
||||
|
||||
options ??= new();
|
||||
|
||||
_sessionTransport = transport;
|
||||
ServerOptions = options;
|
||||
Services = serviceProvider;
|
||||
_serverOnlyEndpointName = $"Server ({options.ServerInfo?.Name ?? DefaultImplementation.Name} {options.ServerInfo?.Version ?? DefaultImplementation.Version})";
|
||||
_endpointName = _serverOnlyEndpointName;
|
||||
_servicesScopePerRequest = options.ScopeRequests;
|
||||
_logger = loggerFactory?.CreateLogger<McpServer>() ?? NullLogger<McpServer>.Instance;
|
||||
|
||||
_clientInfo = options.KnownClientInfo;
|
||||
UpdateEndpointNameWithClientInfo();
|
||||
|
||||
_notificationHandlers = new();
|
||||
_requestHandlers = [];
|
||||
|
||||
// Configure all request handlers based on the supplied options.
|
||||
ServerCapabilities = new();
|
||||
ConfigureInitialize(options);
|
||||
ConfigureTools(options);
|
||||
ConfigurePrompts(options);
|
||||
ConfigureResources(options);
|
||||
ConfigureLogging(options);
|
||||
ConfigureCompletion(options);
|
||||
ConfigureExperimental(options);
|
||||
ConfigurePing();
|
||||
|
||||
// Register any notification handlers that were provided.
|
||||
if (options.Capabilities?.NotificationHandlers is { } notificationHandlers)
|
||||
{
|
||||
_notificationHandlers.RegisterRange(notificationHandlers);
|
||||
}
|
||||
|
||||
// Now that everything has been configured, subscribe to any necessary notifications.
|
||||
if (transport is not StreamableHttpServerTransport streamableHttpTransport || streamableHttpTransport.Stateless is false)
|
||||
{
|
||||
Register(ServerOptions.Capabilities?.Tools?.ToolCollection, NotificationMethods.ToolListChangedNotification);
|
||||
Register(ServerOptions.Capabilities?.Prompts?.PromptCollection, NotificationMethods.PromptListChangedNotification);
|
||||
Register(ServerOptions.Capabilities?.Resources?.ResourceCollection, NotificationMethods.ResourceListChangedNotification);
|
||||
|
||||
void Register<TPrimitive>(McpServerPrimitiveCollection<TPrimitive>? collection, string notificationMethod)
|
||||
where TPrimitive : IMcpServerPrimitive
|
||||
{
|
||||
if (collection is not null)
|
||||
{
|
||||
EventHandler changed = (sender, e) => _ = this.SendNotificationAsync(notificationMethod);
|
||||
collection.Changed += changed;
|
||||
_disposables.Add(() => collection.Changed -= changed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And initialize the session.
|
||||
_sessionHandler = new McpSessionHandler(isServer: true, _sessionTransport, _endpointName!, _requestHandlers, _notificationHandlers, _logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? SessionId => _sessionTransport.SessionId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ServerCapabilities ServerCapabilities { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ClientCapabilities? ClientCapabilities => _clientCapabilities;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Implementation? ClientInfo => _clientInfo;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override McpServerOptions ServerOptions { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IServiceProvider? Services { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override LoggingLevel? LoggingLevel => _loggingLevel?.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task RunAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _started, 1) != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(RunAsync)} must only be called once.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _sessionHandler.ProcessMessagesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
|
||||
=> _sessionHandler.SendRequestAsync(request, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
=> _sessionHandler.SendMessageAsync(message, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler)
|
||||
=> _sessionHandler.RegisterNotificationHandler(method, handler);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
using var _ = await _disposeLock.LockAsync().ConfigureAwait(false);
|
||||
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
_disposables.ForEach(d => d());
|
||||
await _sessionHandler.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void ConfigurePing()
|
||||
{
|
||||
SetHandler(RequestMethods.Ping,
|
||||
async (request, _) => new PingResult(),
|
||||
McpJsonUtilities.JsonContext.Default.JsonNode,
|
||||
McpJsonUtilities.JsonContext.Default.PingResult);
|
||||
}
|
||||
|
||||
private void ConfigureInitialize(McpServerOptions options)
|
||||
{
|
||||
_requestHandlers.Set(RequestMethods.Initialize,
|
||||
async (request, _, _) =>
|
||||
{
|
||||
_clientCapabilities = request?.Capabilities ?? new();
|
||||
_clientInfo = request?.ClientInfo;
|
||||
|
||||
// Use the ClientInfo to update the session EndpointName for logging.
|
||||
UpdateEndpointNameWithClientInfo();
|
||||
_sessionHandler.EndpointName = _endpointName;
|
||||
|
||||
// Negotiate a protocol version. If the server options provide one, use that.
|
||||
// Otherwise, try to use whatever the client requested as long as it's supported.
|
||||
// If it's not supported, fall back to the latest supported version.
|
||||
string? protocolVersion = options.ProtocolVersion;
|
||||
if (protocolVersion is null)
|
||||
{
|
||||
protocolVersion = request?.ProtocolVersion is string clientProtocolVersion && McpSessionHandler.SupportedProtocolVersions.Contains(clientProtocolVersion) ?
|
||||
clientProtocolVersion :
|
||||
McpSessionHandler.LatestProtocolVersion;
|
||||
}
|
||||
|
||||
return new InitializeResult
|
||||
{
|
||||
ProtocolVersion = protocolVersion,
|
||||
Instructions = options.ServerInstructions,
|
||||
ServerInfo = options.ServerInfo ?? DefaultImplementation,
|
||||
Capabilities = ServerCapabilities ?? new(),
|
||||
};
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.InitializeResult);
|
||||
}
|
||||
|
||||
private void ConfigureCompletion(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Completions is not { } completionsCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var completeHandler = completionsCapability.CompleteHandler ?? (static async (_, __) => new CompleteResult());
|
||||
completeHandler = BuildFilterPipeline(completeHandler, options.Filters.CompleteFilters);
|
||||
|
||||
ServerCapabilities.Completions = new()
|
||||
{
|
||||
CompleteHandler = completeHandler
|
||||
};
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.CompletionComplete,
|
||||
ServerCapabilities.Completions.CompleteHandler,
|
||||
McpJsonUtilities.JsonContext.Default.CompleteRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CompleteResult);
|
||||
}
|
||||
|
||||
private void ConfigureExperimental(McpServerOptions options)
|
||||
{
|
||||
ServerCapabilities.Experimental = options.Capabilities?.Experimental;
|
||||
}
|
||||
|
||||
private void ConfigureResources(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Resources is not { } resourcesCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCapabilities.Resources = new();
|
||||
|
||||
var listResourcesHandler = resourcesCapability.ListResourcesHandler ?? (static async (_, __) => new ListResourcesResult());
|
||||
var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler ?? (static async (_, __) => new ListResourceTemplatesResult());
|
||||
var readResourceHandler = resourcesCapability.ReadResourceHandler ?? (static async (request, _) => throw new McpException($"Unknown resource URI: '{request.Params?.Uri}'", McpErrorCode.InvalidParams));
|
||||
var subscribeHandler = resourcesCapability.SubscribeToResourcesHandler ?? (static async (_, __) => new EmptyResult());
|
||||
var unsubscribeHandler = resourcesCapability.UnsubscribeFromResourcesHandler ?? (static async (_, __) => new EmptyResult());
|
||||
var resources = resourcesCapability.ResourceCollection;
|
||||
var listChanged = resourcesCapability.ListChanged;
|
||||
var subscribe = resourcesCapability.Subscribe;
|
||||
|
||||
// Handle resources provided via DI.
|
||||
if (resources is { IsEmpty: false })
|
||||
{
|
||||
var originalListResourcesHandler = listResourcesHandler;
|
||||
listResourcesHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListResourcesResult result = originalListResourcesHandler is not null ?
|
||||
await originalListResourcesHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
foreach (var r in resources)
|
||||
{
|
||||
if (r.ProtocolResource is { } resource)
|
||||
{
|
||||
result.Resources.Add(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var originalListResourceTemplatesHandler = listResourceTemplatesHandler;
|
||||
listResourceTemplatesHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListResourceTemplatesResult result = originalListResourceTemplatesHandler is not null ?
|
||||
await originalListResourceTemplatesHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
foreach (var rt in resources)
|
||||
{
|
||||
if (rt.IsTemplated)
|
||||
{
|
||||
result.ResourceTemplates.Add(rt.ProtocolResourceTemplate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Synthesize read resource handler, which covers both resources and resource templates.
|
||||
var originalReadResourceHandler = readResourceHandler;
|
||||
readResourceHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
if (request.MatchedPrimitive is McpServerResource matchedResource)
|
||||
{
|
||||
if (await matchedResource.ReadAsync(request, cancellationToken).ConfigureAwait(false) is { } result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return await originalReadResourceHandler(request, cancellationToken).ConfigureAwait(false);
|
||||
};
|
||||
|
||||
listChanged = true;
|
||||
|
||||
// TODO: Implement subscribe/unsubscribe logic for resource and resource template collections.
|
||||
// subscribe = true;
|
||||
}
|
||||
|
||||
listResourcesHandler = BuildFilterPipeline(listResourcesHandler, options.Filters.ListResourcesFilters);
|
||||
listResourceTemplatesHandler = BuildFilterPipeline(listResourceTemplatesHandler, options.Filters.ListResourceTemplatesFilters);
|
||||
readResourceHandler = BuildFilterPipeline(readResourceHandler, options.Filters.ReadResourceFilters, handler =>
|
||||
async (request, cancellationToken) =>
|
||||
{
|
||||
// Initial handler that sets MatchedPrimitive
|
||||
if (request.Params?.Uri is { } uri && resources is not null)
|
||||
{
|
||||
// First try an O(1) lookup by exact match.
|
||||
if (resources.TryGetPrimitive(uri, out var resource))
|
||||
{
|
||||
request.MatchedPrimitive = resource;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back to an O(N) lookup, trying to match against each URI template.
|
||||
// The number of templates is controlled by the server developer, and the number is expected to be
|
||||
// not terribly large. If that changes, this can be tweaked to enable a more efficient lookup.
|
||||
foreach (var resourceTemplate in resources)
|
||||
{
|
||||
// Check if this template would handle the request by testing if ReadAsync would succeed
|
||||
if (resourceTemplate.IsTemplated)
|
||||
{
|
||||
// This is a simplified check - a more robust implementation would match the URI pattern
|
||||
// For now, we'll let the actual handler attempt the match
|
||||
request.MatchedPrimitive = resourceTemplate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await handler(request, cancellationToken).ConfigureAwait(false);
|
||||
});
|
||||
subscribeHandler = BuildFilterPipeline(subscribeHandler, options.Filters.SubscribeToResourcesFilters);
|
||||
unsubscribeHandler = BuildFilterPipeline(unsubscribeHandler, options.Filters.UnsubscribeFromResourcesFilters);
|
||||
|
||||
ServerCapabilities.Resources.ListResourcesHandler = listResourcesHandler;
|
||||
ServerCapabilities.Resources.ListResourceTemplatesHandler = listResourceTemplatesHandler;
|
||||
ServerCapabilities.Resources.ReadResourceHandler = readResourceHandler;
|
||||
ServerCapabilities.Resources.ResourceCollection = resources;
|
||||
ServerCapabilities.Resources.SubscribeToResourcesHandler = subscribeHandler;
|
||||
ServerCapabilities.Resources.UnsubscribeFromResourcesHandler = unsubscribeHandler;
|
||||
ServerCapabilities.Resources.ListChanged = listChanged;
|
||||
ServerCapabilities.Resources.Subscribe = subscribe;
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesList,
|
||||
listResourcesHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesTemplatesList,
|
||||
listResourceTemplatesHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesRead,
|
||||
readResourceHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesSubscribe,
|
||||
subscribeHandler,
|
||||
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ResourcesUnsubscribe,
|
||||
unsubscribeHandler,
|
||||
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
}
|
||||
|
||||
private void ConfigurePrompts(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Prompts is not { } promptsCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCapabilities.Prompts = new();
|
||||
|
||||
var listPromptsHandler = promptsCapability.ListPromptsHandler ?? (static async (_, __) => new ListPromptsResult());
|
||||
var getPromptHandler = promptsCapability.GetPromptHandler ?? (static async (request, _) => throw new McpException($"Unknown prompt: '{request.Params?.Name}'", McpErrorCode.InvalidParams));
|
||||
var prompts = promptsCapability.PromptCollection;
|
||||
var listChanged = promptsCapability.ListChanged;
|
||||
|
||||
// Handle tools provided via DI by augmenting the handlers to incorporate them.
|
||||
if (prompts is { IsEmpty: false })
|
||||
{
|
||||
var originalListPromptsHandler = listPromptsHandler;
|
||||
listPromptsHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListPromptsResult result = originalListPromptsHandler is not null ?
|
||||
await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
foreach (var p in prompts)
|
||||
{
|
||||
result.Prompts.Add(p.ProtocolPrompt);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var originalGetPromptHandler = getPromptHandler;
|
||||
getPromptHandler = (request, cancellationToken) =>
|
||||
{
|
||||
if (request.MatchedPrimitive is McpServerPrompt prompt)
|
||||
{
|
||||
return prompt.GetAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
return originalGetPromptHandler(request, cancellationToken);
|
||||
};
|
||||
|
||||
listChanged = true;
|
||||
}
|
||||
|
||||
listPromptsHandler = BuildFilterPipeline(listPromptsHandler, options.Filters.ListPromptsFilters);
|
||||
getPromptHandler = BuildFilterPipeline(getPromptHandler, options.Filters.GetPromptFilters, handler =>
|
||||
(request, cancellationToken) =>
|
||||
{
|
||||
// Initial handler that sets MatchedPrimitive
|
||||
if (request.Params?.Name is { } promptName && prompts is not null &&
|
||||
prompts.TryGetPrimitive(promptName, out var prompt))
|
||||
{
|
||||
request.MatchedPrimitive = prompt;
|
||||
}
|
||||
|
||||
return handler(request, cancellationToken);
|
||||
});
|
||||
|
||||
ServerCapabilities.Prompts.ListPromptsHandler = listPromptsHandler;
|
||||
ServerCapabilities.Prompts.GetPromptHandler = getPromptHandler;
|
||||
ServerCapabilities.Prompts.PromptCollection = prompts;
|
||||
ServerCapabilities.Prompts.ListChanged = listChanged;
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.PromptsList,
|
||||
listPromptsHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.PromptsGet,
|
||||
getPromptHandler,
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptResult);
|
||||
}
|
||||
|
||||
private void ConfigureTools(McpServerOptions options)
|
||||
{
|
||||
if (options.Capabilities?.Tools is not { } toolsCapability)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ServerCapabilities.Tools = new();
|
||||
|
||||
var listToolsHandler = toolsCapability.ListToolsHandler ?? (static async (_, __) => new ListToolsResult());
|
||||
var callToolHandler = toolsCapability.CallToolHandler ?? (static async (request, _) => throw new McpException($"Unknown tool: '{request.Params?.Name}'", McpErrorCode.InvalidParams));
|
||||
var tools = toolsCapability.ToolCollection;
|
||||
var listChanged = toolsCapability.ListChanged;
|
||||
|
||||
// Handle tools provided via DI by augmenting the handlers to incorporate them.
|
||||
if (tools is { IsEmpty: false })
|
||||
{
|
||||
var originalListToolsHandler = listToolsHandler;
|
||||
listToolsHandler = async (request, cancellationToken) =>
|
||||
{
|
||||
ListToolsResult result = originalListToolsHandler is not null ?
|
||||
await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false) :
|
||||
new();
|
||||
|
||||
if (request.Params?.Cursor is null)
|
||||
{
|
||||
foreach (var t in tools)
|
||||
{
|
||||
result.Tools.Add(t.ProtocolTool);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
var originalCallToolHandler = callToolHandler;
|
||||
callToolHandler = (request, cancellationToken) =>
|
||||
{
|
||||
if (request.MatchedPrimitive is McpServerTool tool)
|
||||
{
|
||||
return tool.InvokeAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
return originalCallToolHandler(request, cancellationToken);
|
||||
};
|
||||
|
||||
listChanged = true;
|
||||
}
|
||||
|
||||
listToolsHandler = BuildFilterPipeline(listToolsHandler, options.Filters.ListToolsFilters);
|
||||
callToolHandler = BuildFilterPipeline(callToolHandler, options.Filters.CallToolFilters, handler =>
|
||||
(request, cancellationToken) =>
|
||||
{
|
||||
// Initial handler that sets MatchedPrimitive
|
||||
if (request.Params?.Name is { } toolName && tools is not null &&
|
||||
tools.TryGetPrimitive(toolName, out var tool))
|
||||
{
|
||||
request.MatchedPrimitive = tool;
|
||||
}
|
||||
|
||||
return handler(request, cancellationToken);
|
||||
}, handler =>
|
||||
async (request, cancellationToken) =>
|
||||
{
|
||||
// Final handler that provides exception handling only for tool execution
|
||||
// Only wrap tool execution in try-catch, not tool resolution
|
||||
if (request.MatchedPrimitive is McpServerTool)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await handler(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e) when (e is not OperationCanceledException)
|
||||
{
|
||||
ToolCallError(request.Params?.Name ?? string.Empty, e);
|
||||
|
||||
string errorMessage = e is McpException ?
|
||||
$"An error occurred invoking '{request.Params?.Name}': {e.Message}" :
|
||||
$"An error occurred invoking '{request.Params?.Name}'.";
|
||||
|
||||
return new()
|
||||
{
|
||||
IsError = true,
|
||||
Content = [new TextContentBlock { Text = errorMessage }],
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For unmatched tools, let exceptions bubble up as protocol errors
|
||||
return await handler(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
|
||||
ServerCapabilities.Tools.ListToolsHandler = listToolsHandler;
|
||||
ServerCapabilities.Tools.CallToolHandler = callToolHandler;
|
||||
ServerCapabilities.Tools.ToolCollection = tools;
|
||||
ServerCapabilities.Tools.ListChanged = listChanged;
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ToolsList,
|
||||
listToolsHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsResult);
|
||||
|
||||
SetHandler(
|
||||
RequestMethods.ToolsCall,
|
||||
callToolHandler,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolResult);
|
||||
}
|
||||
|
||||
private void ConfigureLogging(McpServerOptions options)
|
||||
{
|
||||
// We don't require that the handler be provided, as we always store the provided log level to the server.
|
||||
var setLoggingLevelHandler = options.Capabilities?.Logging?.SetLoggingLevelHandler;
|
||||
|
||||
// Apply filters to the handler
|
||||
if (setLoggingLevelHandler is not null)
|
||||
{
|
||||
setLoggingLevelHandler = BuildFilterPipeline(setLoggingLevelHandler, options.Filters.SetLoggingLevelFilters);
|
||||
}
|
||||
|
||||
ServerCapabilities.Logging = new();
|
||||
ServerCapabilities.Logging.SetLoggingLevelHandler = setLoggingLevelHandler;
|
||||
|
||||
_requestHandlers.Set(
|
||||
RequestMethods.LoggingSetLevel,
|
||||
(request, jsonRpcRequest, cancellationToken) =>
|
||||
{
|
||||
// Store the provided level.
|
||||
if (request is not null)
|
||||
{
|
||||
if (_loggingLevel is null)
|
||||
{
|
||||
Interlocked.CompareExchange(ref _loggingLevel, new(request.Level), null);
|
||||
}
|
||||
|
||||
_loggingLevel.Value = request.Level;
|
||||
}
|
||||
|
||||
// If a handler was provided, now delegate to it.
|
||||
if (setLoggingLevelHandler is not null)
|
||||
{
|
||||
return InvokeHandlerAsync(setLoggingLevelHandler, request, jsonRpcRequest, cancellationToken);
|
||||
}
|
||||
|
||||
// Otherwise, consider it handled.
|
||||
return new ValueTask<EmptyResult>(EmptyResult.Instance);
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||
}
|
||||
|
||||
private ValueTask<TResult> InvokeHandlerAsync<TParams, TResult>(
|
||||
McpRequestHandler<TParams, TResult> handler,
|
||||
TParams? args,
|
||||
JsonRpcRequest jsonRpcRequest,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _servicesScopePerRequest ?
|
||||
InvokeScopedAsync(handler, args, jsonRpcRequest, cancellationToken) :
|
||||
handler(new(new DestinationBoundMcpServer(this, jsonRpcRequest.Context?.RelatedTransport), jsonRpcRequest) { Params = args }, cancellationToken);
|
||||
|
||||
async ValueTask<TResult> InvokeScopedAsync(
|
||||
McpRequestHandler<TParams, TResult> handler,
|
||||
TParams? args,
|
||||
JsonRpcRequest jsonRpcRequest,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scope = Services?.GetService<IServiceScopeFactory>()?.CreateAsyncScope();
|
||||
try
|
||||
{
|
||||
return await handler(
|
||||
new RequestContext<TParams>(new DestinationBoundMcpServer(this, jsonRpcRequest.Context?.RelatedTransport), jsonRpcRequest)
|
||||
{
|
||||
Services = scope?.ServiceProvider ?? Services,
|
||||
Params = args
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (scope is not null)
|
||||
{
|
||||
await scope.Value.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetHandler<TParams, TResult>(
|
||||
string method,
|
||||
McpRequestHandler<TParams, TResult> handler,
|
||||
JsonTypeInfo<TParams> requestTypeInfo,
|
||||
JsonTypeInfo<TResult> responseTypeInfo)
|
||||
{
|
||||
_requestHandlers.Set(method,
|
||||
(request, jsonRpcRequest, cancellationToken) =>
|
||||
InvokeHandlerAsync(handler, request, jsonRpcRequest, cancellationToken),
|
||||
requestTypeInfo, responseTypeInfo);
|
||||
}
|
||||
|
||||
private static McpRequestHandler<TParams, TResult> BuildFilterPipeline<TParams, TResult>(
|
||||
McpRequestHandler<TParams, TResult> baseHandler,
|
||||
List<McpRequestFilter<TParams, TResult>> filters,
|
||||
McpRequestFilter<TParams, TResult>? initialHandler = null,
|
||||
McpRequestFilter<TParams, TResult>? finalHandler = null)
|
||||
{
|
||||
var current = baseHandler;
|
||||
|
||||
if (finalHandler is not null)
|
||||
{
|
||||
current = finalHandler(current);
|
||||
}
|
||||
|
||||
for (int i = filters.Count - 1; i >= 0; i--)
|
||||
{
|
||||
current = filters[i](current);
|
||||
}
|
||||
|
||||
if (initialHandler is not null)
|
||||
{
|
||||
current = initialHandler(current);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private void UpdateEndpointNameWithClientInfo()
|
||||
{
|
||||
if (ClientInfo is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_endpointName = $"{_serverOnlyEndpointName}, Client ({ClientInfo.Name} {ClientInfo.Version})";
|
||||
}
|
||||
|
||||
/// <summary>Maps a <see cref="LogLevel"/> to a <see cref="LoggingLevel"/>.</summary>
|
||||
internal static LoggingLevel ToLoggingLevel(LogLevel level) =>
|
||||
level switch
|
||||
{
|
||||
LogLevel.Trace => Protocol.LoggingLevel.Debug,
|
||||
LogLevel.Debug => Protocol.LoggingLevel.Debug,
|
||||
LogLevel.Information => Protocol.LoggingLevel.Info,
|
||||
LogLevel.Warning => Protocol.LoggingLevel.Warning,
|
||||
LogLevel.Error => Protocol.LoggingLevel.Error,
|
||||
LogLevel.Critical => Protocol.LoggingLevel.Critical,
|
||||
_ => Protocol.LoggingLevel.Emergency,
|
||||
};
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "\"{ToolName}\" threw an unhandled exception.")]
|
||||
private partial void ToolCallError(string toolName, Exception exception);
|
||||
}
|
||||
|
|
@ -15,8 +15,8 @@ namespace ModelContextProtocol.Server;
|
|||
/// <see cref="McpServerPrompt"/> is an abstract base class that represents an MCP prompt for use in the server (as opposed
|
||||
/// to <see cref="Prompt"/>, which provides the protocol representation of a prompt, and <see cref="McpClientPrompt"/>, which
|
||||
/// provides a client-side representation of a prompt). Instances of <see cref="McpServerPrompt"/> can be added into a
|
||||
/// <see cref="IServiceCollection"/> to be picked up automatically when <see cref="McpServerFactory"/> is used to create
|
||||
/// an <see cref="IMcpServer"/>, or added into a <see cref="McpServerPrimitiveCollection{McpServerPrompt}"/>.
|
||||
/// <see cref="IServiceCollection"/> to be picked up automatically when <see cref="McpServer"/> is used to create
|
||||
/// an <see cref="McpServer"/>, or added into a <see cref="McpServerPrimitiveCollection{McpServerPrompt}"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Most commonly, <see cref="McpServerPrompt"/> instances are created using the static <see cref="M:McpServerPrompt.Create"/> methods.
|
||||
|
|
@ -34,7 +34,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="CancellationToken"/> parameters are automatically bound to a <see cref="CancellationToken"/> provided by the
|
||||
/// <see cref="IMcpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="McpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="RequestId"/>.
|
||||
/// </description>
|
||||
/// </item>
|
||||
|
|
@ -45,7 +45,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IMcpServer"/> parameters are bound directly to the <see cref="IMcpServer"/> instance associated
|
||||
/// <see cref="McpServer"/> parameters are bound directly to the <see cref="McpServer"/> instance associated
|
||||
/// with this request's <see cref="RequestContext{CallPromptRequestParams}"/>. Such parameters may be used to understand
|
||||
/// what server is being used to process the request, and to interact with the client issuing the request to that server.
|
||||
/// </description>
|
||||
|
|
@ -210,7 +210,7 @@ public abstract class McpServerPrompt : IMcpServerPrimitive
|
|||
/// <exception cref="ArgumentNullException"><paramref name="function"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// Unlike the other overloads of Create, the <see cref="McpServerPrompt"/> created by <see cref="Create(AIFunction, McpServerPromptCreateOptions)"/>
|
||||
/// does not provide all of the special parameter handling for MCP-specific concepts, like <see cref="IMcpServer"/>.
|
||||
/// does not provide all of the special parameter handling for MCP-specific concepts, like <see cref="McpServer"/>.
|
||||
/// </remarks>
|
||||
public static McpServerPrompt Create(
|
||||
AIFunction function,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="CancellationToken"/> parameters are automatically bound to a <see cref="CancellationToken"/> provided by the
|
||||
/// <see cref="IMcpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="McpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="RequestId"/>.
|
||||
/// </description>
|
||||
/// </item>
|
||||
|
|
@ -36,7 +36,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IMcpServer"/> parameters are bound directly to the <see cref="IMcpServer"/> instance associated
|
||||
/// <see cref="McpServer"/> parameters are bound directly to the <see cref="McpServer"/> instance associated
|
||||
/// with this request's <see cref="RequestContext{CallPromptRequestParams}"/>. Such parameters may be used to understand
|
||||
/// what server is being used to process the request, and to interact with the client issuing the request to that server.
|
||||
/// </description>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// <see cref="McpServerResource"/> is an abstract base class that represents an MCP resource for use in the server (as opposed
|
||||
/// to <see cref="Resource"/> or <see cref="ResourceTemplate"/>, which provide the protocol representations of a resource). Instances of
|
||||
/// <see cref="McpServerResource"/> can be added into a <see cref="IServiceCollection"/> to be picked up automatically when
|
||||
/// <see cref="McpServerFactory"/> is used to create an <see cref="IMcpServer"/>, or added into a <see cref="McpServerPrimitiveCollection{McpServerResource}"/>.
|
||||
/// <see cref="McpServer"/> is used to create an <see cref="McpServer"/>, or added into a <see cref="McpServerPrimitiveCollection{McpServerResource}"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Most commonly, <see cref="McpServerResource"/> instances are created using the static <see cref="M:McpServerResource.Create"/> methods.
|
||||
|
|
@ -35,7 +35,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="CancellationToken"/> parameters are automatically bound to a <see cref="CancellationToken"/> provided by the
|
||||
/// <see cref="IMcpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="McpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="RequestId"/>.
|
||||
/// </description>
|
||||
/// </item>
|
||||
|
|
@ -46,7 +46,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IMcpServer"/> parameters are bound directly to the <see cref="IMcpServer"/> instance associated
|
||||
/// <see cref="McpServer"/> parameters are bound directly to the <see cref="McpServer"/> instance associated
|
||||
/// with this request's <see cref="RequestContext{ReadResourceRequestParams}"/>. Such parameters may be used to understand
|
||||
/// what server is being used to process the request, and to interact with the client issuing the request to that server.
|
||||
/// </description>
|
||||
|
|
@ -232,7 +232,7 @@ public abstract class McpServerResource : IMcpServerPrimitive
|
|||
/// <exception cref="ArgumentNullException"><paramref name="function"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// Unlike the other overloads of Create, the <see cref="McpServerResource"/> created by <see cref="Create(AIFunction, McpServerResourceCreateOptions)"/>
|
||||
/// does not provide all of the special parameter handling for MCP-specific concepts, like <see cref="IMcpServer"/>.
|
||||
/// does not provide all of the special parameter handling for MCP-specific concepts, like <see cref="McpServer"/>.
|
||||
/// </remarks>
|
||||
public static McpServerResource Create(
|
||||
AIFunction function,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="CancellationToken"/> parameters are automatically bound to a <see cref="CancellationToken"/> provided by the
|
||||
/// <see cref="IMcpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="McpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="RequestId"/>.
|
||||
/// </description>
|
||||
/// </item>
|
||||
|
|
@ -34,7 +34,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IMcpServer"/> parameters are bound directly to the <see cref="IMcpServer"/> instance associated
|
||||
/// <see cref="McpServer"/> parameters are bound directly to the <see cref="McpServer"/> instance associated
|
||||
/// with this request's <see cref="RequestContext{ReadResourceRequestParams}"/>. Such parameters may be used to understand
|
||||
/// what server is being used to process the request, and to interact with the client issuing the request to that server.
|
||||
/// </description>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ namespace ModelContextProtocol.Server;
|
|||
/// <see cref="McpServerTool"/> is an abstract base class that represents an MCP tool for use in the server (as opposed
|
||||
/// to <see cref="Tool"/>, which provides the protocol representation of a tool, and <see cref="McpClientTool"/>, which
|
||||
/// provides a client-side representation of a tool). Instances of <see cref="McpServerTool"/> can be added into a
|
||||
/// <see cref="IServiceCollection"/> to be picked up automatically when <see cref="McpServerFactory"/> is used to create
|
||||
/// an <see cref="IMcpServer"/>, or added into a <see cref="McpServerPrimitiveCollection{McpServerTool}"/>.
|
||||
/// <see cref="IServiceCollection"/> to be picked up automatically when <see cref="McpServer"/> is used to create
|
||||
/// an <see cref="McpServer"/>, or added into a <see cref="McpServerPrimitiveCollection{McpServerTool}"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Most commonly, <see cref="McpServerTool"/> instances are created using the static <see cref="M:McpServerTool.Create"/> methods.
|
||||
|
|
@ -35,7 +35,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="CancellationToken"/> parameters are automatically bound to a <see cref="CancellationToken"/> provided by the
|
||||
/// <see cref="IMcpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="McpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="RequestId"/>. The parameter is not included in the generated JSON schema.
|
||||
/// </description>
|
||||
/// </item>
|
||||
|
|
@ -47,7 +47,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IMcpServer"/> parameters are not included in the JSON schema and are bound directly to the <see cref="IMcpServer"/>
|
||||
/// <see cref="McpServer"/> parameters are not included in the JSON schema and are bound directly to the <see cref="McpServer"/>
|
||||
/// instance associated with this request's <see cref="RequestContext{CallToolRequestParams}"/>. Such parameters may be used to understand
|
||||
/// what server is being used to process the request, and to interact with the client issuing the request to that server.
|
||||
/// </description>
|
||||
|
|
@ -212,7 +212,7 @@ public abstract class McpServerTool : IMcpServerPrimitive
|
|||
/// <exception cref="ArgumentNullException"><paramref name="function"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// Unlike the other overloads of Create, the <see cref="McpServerTool"/> created by <see cref="Create(AIFunction, McpServerToolCreateOptions)"/>
|
||||
/// does not provide all of the special parameter handling for MCP-specific concepts, like <see cref="IMcpServer"/>.
|
||||
/// does not provide all of the special parameter handling for MCP-specific concepts, like <see cref="McpServer"/>.
|
||||
/// </remarks>
|
||||
public static McpServerTool Create(
|
||||
AIFunction function,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="CancellationToken"/> parameters are automatically bound to a <see cref="CancellationToken"/> provided by the
|
||||
/// <see cref="IMcpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="McpServer"/> and that respects any <see cref="CancelledNotificationParams"/>s sent by the client for this operation's
|
||||
/// <see cref="RequestId"/>. The parameter is not included in the generated JSON schema.
|
||||
/// </description>
|
||||
/// </item>
|
||||
|
|
@ -38,7 +38,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <see cref="IMcpServer"/> parameters are not included in the JSON schema and are bound directly to the <see cref="IMcpServer"/>
|
||||
/// <see cref="McpServer"/> parameters are not included in the JSON schema and are bound directly to the <see cref="McpServer"/>
|
||||
/// instance associated with this request's <see cref="RequestContext{CallToolRequestParams}"/>. Such parameters may be used to understand
|
||||
/// what server is being used to process the request, and to interact with the client issuing the request to that server.
|
||||
/// </description>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace ModelContextProtocol.Server;
|
|||
public sealed class RequestContext<TParams>
|
||||
{
|
||||
/// <summary>The server with which this instance is associated.</summary>
|
||||
private IMcpServer _server;
|
||||
private McpServer _server;
|
||||
|
||||
private IDictionary<string, object?>? _items;
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ public sealed class RequestContext<TParams>
|
|||
/// </summary>
|
||||
/// <param name="server">The server with which this instance is associated.</param>
|
||||
/// <param name="jsonRpcRequest">The JSON-RPC request associated with this context.</param>
|
||||
public RequestContext(IMcpServer server, JsonRpcRequest jsonRpcRequest)
|
||||
public RequestContext(McpServer server, JsonRpcRequest jsonRpcRequest)
|
||||
{
|
||||
Throw.IfNull(server);
|
||||
Throw.IfNull(jsonRpcRequest);
|
||||
|
|
@ -36,7 +36,7 @@ public sealed class RequestContext<TParams>
|
|||
}
|
||||
|
||||
/// <summary>Gets or sets the server with which this instance is associated.</summary>
|
||||
public IMcpServer Server
|
||||
public McpServer Server
|
||||
{
|
||||
get => _server;
|
||||
set
|
||||
|
|
@ -63,10 +63,10 @@ public sealed class RequestContext<TParams>
|
|||
|
||||
/// <summary>Gets or sets the services associated with this request.</summary>
|
||||
/// <remarks>
|
||||
/// This may not be the same instance stored in <see cref="IMcpServer.Services"/>
|
||||
/// This may not be the same instance stored in <see cref="McpServer.Services"/>
|
||||
/// if <see cref="McpServerOptions.ScopeRequests"/> was true, in which case this
|
||||
/// might be a scoped <see cref="IServiceProvider"/> derived from the server's
|
||||
/// <see cref="IMcpServer.Services"/>.
|
||||
/// <see cref="McpServer.Services"/>.
|
||||
/// </remarks>
|
||||
public IServiceProvider? Services { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ namespace ModelContextProtocol.Server;
|
|||
|
||||
/// <summary>Augments a service provider with additional request-related services.</summary>
|
||||
internal sealed class RequestServiceProvider<TRequestParams>(RequestContext<TRequestParams> request) :
|
||||
IServiceProvider, IKeyedServiceProvider, IServiceProviderIsService, IServiceProviderIsKeyedService,
|
||||
IDisposable, IAsyncDisposable
|
||||
IServiceProvider, IKeyedServiceProvider, IServiceProviderIsService, IServiceProviderIsKeyedService,
|
||||
IDisposable, IAsyncDisposable
|
||||
where TRequestParams : RequestParams
|
||||
{
|
||||
private readonly IServiceProvider? _innerServices = request.Services;
|
||||
|
|
@ -18,14 +18,19 @@ internal sealed class RequestServiceProvider<TRequestParams>(RequestContext<TReq
|
|||
/// <summary>Gets whether the specified type is in the list of additional types this service provider wraps around the one in a provided request's services.</summary>
|
||||
public static bool IsAugmentedWith(Type serviceType) =>
|
||||
serviceType == typeof(RequestContext<TRequestParams>) ||
|
||||
serviceType == typeof(McpServer) ||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
serviceType == typeof(IMcpServer) ||
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
serviceType == typeof(IProgress<ProgressNotificationValue>) ||
|
||||
serviceType == typeof(ClaimsPrincipal);
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? GetService(Type serviceType) =>
|
||||
serviceType == typeof(RequestContext<TRequestParams>) ? request :
|
||||
serviceType == typeof(IMcpServer) ? request.Server :
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
serviceType == typeof(McpServer) || serviceType == typeof(IMcpServer) ? request.Server :
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
serviceType == typeof(IProgress<ProgressNotificationValue>) ?
|
||||
(request.Params?.ProgressToken is { } progressToken ? new TokenProgress(request.Server, progressToken) : NullProgress.Instance) :
|
||||
serviceType == typeof(ClaimsPrincipal) ? request.User :
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ public sealed class StdioServerTransport : StreamServerTransport
|
|||
{
|
||||
Throw.IfNull(serverOptions);
|
||||
|
||||
return serverOptions.ServerInfo?.Name ?? McpServer.DefaultImplementation.Name;
|
||||
return serverOptions.ServerInfo?.Name ?? McpServerImpl.DefaultImplementation.Name;
|
||||
}
|
||||
|
||||
// Neither WindowsConsoleStream nor UnixConsoleStream respect CancellationTokens or cancel any I/O on Dispose.
|
||||
|
|
|
|||
Loading…
Reference in a new issue