parent
36a70a03f8
commit
5cc7c00456
16 changed files with 480 additions and 22 deletions
|
|
@ -46,7 +46,7 @@ builder.Services
|
||||||
{
|
{
|
||||||
subscriptions.Add(uri);
|
subscriptions.Add(uri);
|
||||||
|
|
||||||
await ctx.Server.RequestSamplingAsync([
|
await ctx.Server.SampleAsync([
|
||||||
new ChatMessage(ChatRole.System, "You are a helpful test server"),
|
new ChatMessage(ChatRole.System, "You are a helpful test server"),
|
||||||
new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"),
|
new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ public class SampleLlmTool
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
|
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
|
||||||
var sampleResult = await server.RequestSamplingAsync(samplingParams, cancellationToken);
|
var sampleResult = await server.SampleAsync(samplingParams, cancellationToken);
|
||||||
|
|
||||||
return $"LLM sampling result: {sampleResult.Content.Text}";
|
return $"LLM sampling result: {sampleResult.Content.Text}";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ public sealed class SampleLlmTool
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
|
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
|
||||||
var sampleResult = await thisServer.RequestSamplingAsync(samplingParams, cancellationToken);
|
var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken);
|
||||||
|
|
||||||
return $"LLM sampling result: {sampleResult.Content.Text}";
|
return $"LLM sampling result: {sampleResult.Content.Text}";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,20 @@ internal sealed partial class McpClient : McpEndpoint, IMcpClient
|
||||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||||
McpJsonUtilities.JsonContext.Default.ListRootsResult);
|
McpJsonUtilities.JsonContext.Default.ListRootsResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (capabilities.Elicitation is { } elicitationCapability)
|
||||||
|
{
|
||||||
|
if (elicitationCapability.ElicitationHandler is not { } elicitationHandler)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Elicitation capability was set but it did not provide a handler.");
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestHandlers.Set(
|
||||||
|
RequestMethods.ElicitationCreate,
|
||||||
|
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
|
||||||
|
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||||
|
McpJsonUtilities.JsonContext.Default.ElicitResult);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,8 @@ public static partial class McpJsonUtilities
|
||||||
[JsonSerializable(typeof(CompleteResult))]
|
[JsonSerializable(typeof(CompleteResult))]
|
||||||
[JsonSerializable(typeof(CreateMessageRequestParams))]
|
[JsonSerializable(typeof(CreateMessageRequestParams))]
|
||||||
[JsonSerializable(typeof(CreateMessageResult))]
|
[JsonSerializable(typeof(CreateMessageResult))]
|
||||||
|
[JsonSerializable(typeof(ElicitRequestParams))]
|
||||||
|
[JsonSerializable(typeof(ElicitResult))]
|
||||||
[JsonSerializable(typeof(EmptyResult))]
|
[JsonSerializable(typeof(EmptyResult))]
|
||||||
[JsonSerializable(typeof(GetPromptRequestParams))]
|
[JsonSerializable(typeof(GetPromptRequestParams))]
|
||||||
[JsonSerializable(typeof(GetPromptResult))]
|
[JsonSerializable(typeof(GetPromptResult))]
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,13 @@ public class ClientCapabilities
|
||||||
[JsonPropertyName("sampling")]
|
[JsonPropertyName("sampling")]
|
||||||
public SamplingCapability? Sampling { get; set; }
|
public SamplingCapability? Sampling { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the client's elicitation capability, which indicates whether the client
|
||||||
|
/// supports elicitation of additional information from the user on behalf of the server.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("elicitation")]
|
||||||
|
public ElicitationCapability? Elicitation { get; set; }
|
||||||
|
|
||||||
/// <summary>Gets or sets notification handlers to register with the client.</summary>
|
/// <summary>Gets or sets notification handlers to register with the client.</summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
|
||||||
230
src/ModelContextProtocol/Protocol/ElicitRequestParams.cs
Normal file
230
src/ModelContextProtocol/Protocol/ElicitRequestParams.cs
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Protocol;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a message issued from the server to elicit additional information from the user via the client.
|
||||||
|
/// </summary>
|
||||||
|
public class ElicitRequestParams
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the message to present to the user.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the requested schema.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// May be one of <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>, or <see cref="EnumSchema"/>.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("requestedSchema")]
|
||||||
|
[field: MaybeNull]
|
||||||
|
public RequestSchema RequestedSchema
|
||||||
|
{
|
||||||
|
get => field ??= new RequestSchema();
|
||||||
|
set => field = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Represents a request schema used in an elicitation request.</summary>
|
||||||
|
public class RequestSchema
|
||||||
|
{
|
||||||
|
/// <summary>Gets the type of the schema.</summary>
|
||||||
|
/// <remarks>This is always "object".</remarks>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type => "object";
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the properties of the schema.</summary>
|
||||||
|
[JsonPropertyName("properties")]
|
||||||
|
[field: MaybeNull]
|
||||||
|
public IDictionary<string, PrimitiveSchemaDefinition> Properties
|
||||||
|
{
|
||||||
|
get => field ??= new Dictionary<string, PrimitiveSchemaDefinition>();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the required properties of the schema.</summary>
|
||||||
|
[JsonPropertyName("required")]
|
||||||
|
public IList<string>? Required { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents restricted subset of JSON Schema:
|
||||||
|
/// <see cref="StringSchema"/>, <see cref="NumberSchema"/>, <see cref="BooleanSchema"/>, or <see cref="EnumSchema"/>.
|
||||||
|
/// </summary>
|
||||||
|
[JsonDerivedType(typeof(BooleanSchema))]
|
||||||
|
[JsonDerivedType(typeof(EnumSchema))]
|
||||||
|
[JsonDerivedType(typeof(NumberSchema))]
|
||||||
|
[JsonDerivedType(typeof(StringSchema))]
|
||||||
|
public abstract class PrimitiveSchemaDefinition
|
||||||
|
{
|
||||||
|
protected private PrimitiveSchemaDefinition()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Represents a schema for a string type.</summary>
|
||||||
|
public sealed class StringSchema : PrimitiveSchemaDefinition
|
||||||
|
{
|
||||||
|
/// <summary>Gets the type of the schema.</summary>
|
||||||
|
/// <remarks>This is always "string".</remarks>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type => "string";
|
||||||
|
|
||||||
|
/// <summary>Gets or sets a title for the string.</summary>
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets a description for the string.</summary>
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the minimum length for the string.</summary>
|
||||||
|
[JsonPropertyName("minLength")]
|
||||||
|
public int? MinLength
|
||||||
|
{
|
||||||
|
get => field;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value), "Minimum length cannot be negative.");
|
||||||
|
}
|
||||||
|
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the maximum length for the string.</summary>
|
||||||
|
[JsonPropertyName("maxLength")]
|
||||||
|
public int? MaxLength
|
||||||
|
{
|
||||||
|
get => field;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(value), "Maximum length cannot be negative.");
|
||||||
|
}
|
||||||
|
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets or sets a specific format for the string ("email", "uri", "date", or "date-time").</summary>
|
||||||
|
[JsonPropertyName("format")]
|
||||||
|
public string? Format
|
||||||
|
{
|
||||||
|
get => field;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is not (null or "email" or "uri" or "date" or "date-time"))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Format must be 'email', 'uri', 'date', or 'date-time'.", nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Represents a schema for a number or integer type.</summary>
|
||||||
|
public sealed class NumberSchema : PrimitiveSchemaDefinition
|
||||||
|
{
|
||||||
|
/// <summary>Gets the type of the schema.</summary>
|
||||||
|
/// <remarks>This should be "number" or "integer".</remarks>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
[field: MaybeNull]
|
||||||
|
public string Type
|
||||||
|
{
|
||||||
|
get => field ??= "number";
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is not ("number" or "integer"))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Type must be 'number' or 'integer'.", nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets or sets a title for the number input.</summary>
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets a description for the number input.</summary>
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the minimum allowed value.</summary>
|
||||||
|
[JsonPropertyName("minimum")]
|
||||||
|
public double? Minimum { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the maximum allowed value.</summary>
|
||||||
|
[JsonPropertyName("maximum")]
|
||||||
|
public double? Maximum { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Represents a schema for a Boolean type.</summary>
|
||||||
|
public sealed class BooleanSchema : PrimitiveSchemaDefinition
|
||||||
|
{
|
||||||
|
/// <summary>Gets the type of the schema.</summary>
|
||||||
|
/// <remarks>This is always "boolean".</remarks>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type => "boolean";
|
||||||
|
|
||||||
|
/// <summary>Gets or sets a title for the Boolean.</summary>
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets a description for the Boolean.</summary>
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the default value for the Boolean.</summary>
|
||||||
|
[JsonPropertyName("default")]
|
||||||
|
public bool? Default { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Represents a schema for an enum type.</summary>
|
||||||
|
public sealed class EnumSchema : PrimitiveSchemaDefinition
|
||||||
|
{
|
||||||
|
/// <summary>Gets the type of the schema.</summary>
|
||||||
|
/// <remarks>This is always "string".</remarks>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type => "string";
|
||||||
|
|
||||||
|
/// <summary>Gets or sets a title for the enum.</summary>
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets a description for the enum.</summary>
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the list of allowed string values for the enum.</summary>
|
||||||
|
[JsonPropertyName("enum")]
|
||||||
|
[field: MaybeNull]
|
||||||
|
public IList<string> Enum
|
||||||
|
{
|
||||||
|
get => field ??= [];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Throw.IfNull(value);
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets or sets optional display names corresponding to the enum values.</summary>
|
||||||
|
[JsonPropertyName("enumNames")]
|
||||||
|
public IList<string>? EnumNames { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/ModelContextProtocol/Protocol/ElicitResult.cs
Normal file
41
src/ModelContextProtocol/Protocol/ElicitResult.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Protocol;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the client's response to an elicitation request.
|
||||||
|
/// </summary>
|
||||||
|
public class ElicitResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user action in response to the elicitation.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>
|
||||||
|
/// <term>"accept"</term>
|
||||||
|
/// <description>User submitted the form/confirmed the action</description>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <term>"decline"</term>
|
||||||
|
/// <description>User explicitly declined the action</description>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <term>"cancel"</term>
|
||||||
|
/// <description>User dismissed without making an explicit choice</description>
|
||||||
|
/// </item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("action")]
|
||||||
|
public string Action { get; set; } = "cancel";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the submitted form data.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is typically omitted if the action is "cancel" or "decline".
|
||||||
|
/// </remarks>
|
||||||
|
[JsonPropertyName("content")]
|
||||||
|
public JsonElement? Content { get; set; }
|
||||||
|
}
|
||||||
36
src/ModelContextProtocol/Protocol/ElicitationCapability.cs
Normal file
36
src/ModelContextProtocol/Protocol/ElicitationCapability.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Protocol;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the capability for a client to provide server-requested additional information during interactions.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// This capability enables the MCP client to respond to elicitation requests from an MCP server.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// When this capability is enabled, an MCP server can request the client to provide additional information
|
||||||
|
/// during interactions. The client must set a <see cref="ElicitationHandler"/> to process these requests.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public class ElicitationCapability
|
||||||
|
{
|
||||||
|
// Currently empty in the spec, but may be extended in the future.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the handler for processing <see cref="RequestMethods.ElicitationCreate"/> requests.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// This handler function is called when an MCP server requests the client to provide additional
|
||||||
|
/// information during interactions. The client must set this property for the elicitation capability to work.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// The handler receives message parameters and a cancellation token.
|
||||||
|
/// It should return a <see cref="ElicitResult"/> containing the response to the elicitation request.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[JsonIgnore]
|
||||||
|
public Func<ElicitRequestParams?, CancellationToken, ValueTask<ElicitResult>>? ElicitationHandler { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -93,6 +93,15 @@ public static class RequestMethods
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public const string SamplingCreateMessage = "sampling/createMessage";
|
public const string SamplingCreateMessage = "sampling/createMessage";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the request method sent from the client to the server to elicit additional information from the user via the client.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This request is used when the server needs more information from the client to proceed with a task or interaction.
|
||||||
|
/// Servers can request structured data from users, with optional JSON schemas to validate responses.
|
||||||
|
/// </remarks>
|
||||||
|
public const string ElicitationCreate = "elicitation/create";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the request method sent from the client to the server when it first connects, asking it initialize.
|
/// The name of the request method sent from the client to the server when it first connects, asking it initialize.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,13 @@ public static class McpServerExtensions
|
||||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</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>
|
/// <returns>A task containing the sampling result from the client.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
||||||
/// <exception cref="ArgumentException">The client does not support sampling.</exception>
|
/// <exception cref="InvalidOperationException">The client does not support sampling.</exception>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This method requires the client to support sampling capabilities.
|
/// This method requires the client to support sampling capabilities.
|
||||||
/// It allows detailed control over sampling parameters including messages, system prompt, temperature,
|
/// It allows detailed control over sampling parameters including messages, system prompt, temperature,
|
||||||
/// and token limits.
|
/// and token limits.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static ValueTask<CreateMessageResult> RequestSamplingAsync(
|
public static ValueTask<CreateMessageResult> SampleAsync(
|
||||||
this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default)
|
this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Throw.IfNull(server);
|
Throw.IfNull(server);
|
||||||
|
|
@ -50,12 +50,12 @@ public static class McpServerExtensions
|
||||||
/// <returns>A task containing the chat response from the model.</returns>
|
/// <returns>A task containing the chat response from the model.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
|
||||||
/// <exception cref="ArgumentException">The client does not support sampling.</exception>
|
/// <exception cref="InvalidOperationException">The client does not support sampling.</exception>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This method converts the provided chat messages into a format suitable for the sampling API,
|
/// 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.
|
/// handling different content types such as text, images, and audio.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static async Task<ChatResponse> RequestSamplingAsync(
|
public static async Task<ChatResponse> SampleAsync(
|
||||||
this IMcpServer server,
|
this IMcpServer server,
|
||||||
IEnumerable<ChatMessage> messages, ChatOptions? options = default, CancellationToken cancellationToken = default)
|
IEnumerable<ChatMessage> messages, ChatOptions? options = default, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|
@ -125,7 +125,7 @@ public static class McpServerExtensions
|
||||||
modelPreferences = new() { Hints = [new() { Name = modelId }] };
|
modelPreferences = new() { Hints = [new() { Name = modelId }] };
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await server.RequestSamplingAsync(new()
|
var result = await server.SampleAsync(new()
|
||||||
{
|
{
|
||||||
Messages = samplingMessages,
|
Messages = samplingMessages,
|
||||||
MaxTokens = options?.MaxOutputTokens,
|
MaxTokens = options?.MaxOutputTokens,
|
||||||
|
|
@ -152,7 +152,7 @@ public static class McpServerExtensions
|
||||||
/// <param name="server">The server to be wrapped as an <see cref="IChatClient"/>.</param>
|
/// <param name="server">The server to be wrapped as an <see cref="IChatClient"/>.</param>
|
||||||
/// <returns>The <see cref="IChatClient"/> that can be used to issue sampling requests to the client.</returns>
|
/// <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="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
||||||
/// <exception cref="ArgumentException">The client does not support sampling.</exception>
|
/// <exception cref="InvalidOperationException">The client does not support sampling.</exception>
|
||||||
public static IChatClient AsSamplingChatClient(this IMcpServer server)
|
public static IChatClient AsSamplingChatClient(this IMcpServer server)
|
||||||
{
|
{
|
||||||
Throw.IfNull(server);
|
Throw.IfNull(server);
|
||||||
|
|
@ -179,7 +179,7 @@ public static class McpServerExtensions
|
||||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</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>
|
/// <returns>A task containing the list of roots exposed by the client.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
||||||
/// <exception cref="ArgumentException">The client does not support roots.</exception>
|
/// <exception cref="InvalidOperationException">The client does not support roots.</exception>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This method requires the client to support the roots capability.
|
/// This method requires the client to support the roots capability.
|
||||||
/// Root resources allow clients to expose a hierarchical structure of resources that can be
|
/// Root resources allow clients to expose a hierarchical structure of resources that can be
|
||||||
|
|
@ -200,6 +200,32 @@ public static class McpServerExtensions
|
||||||
cancellationToken: cancellationToken);
|
cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requests additional information from the user via the client, allowing the server to elicit structured data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="server">The server initiating the request.</param>
|
||||||
|
/// <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="ArgumentNullException"><paramref name="server"/> is <see langword="null"/>.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">The client does not support elicitation.</exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method requires the client to support the elicitation capability.
|
||||||
|
/// </remarks>
|
||||||
|
public static ValueTask<ElicitResult> ElicitAsync(
|
||||||
|
this IMcpServer server, ElicitRequestParams request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Throw.IfNull(server);
|
||||||
|
ThrowIfElicitationUnsupported(server);
|
||||||
|
|
||||||
|
return server.SendRequestAsync(
|
||||||
|
RequestMethods.ElicitationCreate,
|
||||||
|
request,
|
||||||
|
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||||
|
McpJsonUtilities.JsonContext.Default.ElicitResult,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
private static void ThrowIfSamplingUnsupported(IMcpServer server)
|
private static void ThrowIfSamplingUnsupported(IMcpServer server)
|
||||||
{
|
{
|
||||||
if (server.ClientCapabilities?.Sampling is null)
|
if (server.ClientCapabilities?.Sampling is null)
|
||||||
|
|
@ -226,12 +252,25 @@ public static class McpServerExtensions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
/// <summary>Provides an <see cref="IChatClient"/> implementation that's implemented via client sampling.</summary>
|
||||||
private sealed class SamplingChatClient(IMcpServer server) : IChatClient
|
private sealed class SamplingChatClient(IMcpServer server) : IChatClient
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
|
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||||
server.RequestSamplingAsync(messages, options, cancellationToken);
|
server.SampleAsync(messages, options, cancellationToken);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
async IAsyncEnumerable<ChatResponseUpdate> IChatClient.GetStreamingResponseAsync(
|
async IAsyncEnumerable<ChatResponseUpdate> IChatClient.GetStreamingResponseAsync(
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,11 @@ public class TestServerTransport : ITransport
|
||||||
if (message is JsonRpcRequest request)
|
if (message is JsonRpcRequest request)
|
||||||
{
|
{
|
||||||
if (request.Method == RequestMethods.RootsList)
|
if (request.Method == RequestMethods.RootsList)
|
||||||
await ListRoots(request, cancellationToken);
|
await ListRootsAsync(request, cancellationToken);
|
||||||
else if (request.Method == RequestMethods.SamplingCreateMessage)
|
else if (request.Method == RequestMethods.SamplingCreateMessage)
|
||||||
await Sampling(request, cancellationToken);
|
await SamplingAsync(request, cancellationToken);
|
||||||
|
else if (request.Method == RequestMethods.ElicitationCreate)
|
||||||
|
await ElicitAsync(request, cancellationToken);
|
||||||
else
|
else
|
||||||
await WriteMessageAsync(request, cancellationToken);
|
await WriteMessageAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +55,7 @@ public class TestServerTransport : ITransport
|
||||||
OnMessageSent?.Invoke(message);
|
OnMessageSent?.Invoke(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ListRoots(JsonRpcRequest request, CancellationToken cancellationToken)
|
private async Task ListRootsAsync(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await WriteMessageAsync(new JsonRpcResponse
|
await WriteMessageAsync(new JsonRpcResponse
|
||||||
{
|
{
|
||||||
|
|
@ -65,7 +67,7 @@ public class TestServerTransport : ITransport
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Sampling(JsonRpcRequest request, CancellationToken cancellationToken)
|
private async Task SamplingAsync(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await WriteMessageAsync(new JsonRpcResponse
|
await WriteMessageAsync(new JsonRpcResponse
|
||||||
{
|
{
|
||||||
|
|
@ -74,6 +76,15 @@ public class TestServerTransport : ITransport
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ElicitAsync(JsonRpcRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await WriteMessageAsync(new JsonRpcResponse
|
||||||
|
{
|
||||||
|
Id = request.Id,
|
||||||
|
Result = JsonSerializer.SerializeToNode(new ElicitResult { Action = "decline" }, McpJsonUtilities.DefaultOptions),
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task WriteMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
private async Task WriteMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _messageChannel.Writer.WriteAsync(message, cancellationToken);
|
await _messageChannel.Writer.WriteAsync(message, cancellationToken);
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,26 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
|
||||||
Assert.Equal("Server to client requests are not supported in stateless mode.", toolContent.Text);
|
Assert.Equal("Server to client requests are not supported in stateless mode.", toolContent.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ElicitRequest_Fails_WithInvalidOperationException()
|
||||||
|
{
|
||||||
|
await StartAsync();
|
||||||
|
|
||||||
|
var mcpClientOptions = new McpClientOptions();
|
||||||
|
mcpClientOptions.Capabilities = new();
|
||||||
|
mcpClientOptions.Capabilities.Elicitation ??= new();
|
||||||
|
mcpClientOptions.Capabilities.Elicitation.ElicitationHandler = (_, _) =>
|
||||||
|
{
|
||||||
|
throw new UnreachableException();
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var client = await ConnectMcpClientAsync(mcpClientOptions);
|
||||||
|
|
||||||
|
var toolResponse = await client.CallToolAsync("testElicitationErrors", cancellationToken: TestContext.Current.CancellationToken);
|
||||||
|
var toolContent = Assert.Single(toolResponse.Content);
|
||||||
|
Assert.Equal("Server to client requests are not supported in stateless mode.", toolContent.Text);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UnsolicitedNotification_Fails_WithInvalidOperationException()
|
public async Task UnsolicitedNotification_Fails_WithInvalidOperationException()
|
||||||
{
|
{
|
||||||
|
|
@ -184,7 +204,7 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
|
||||||
var asSamplingChatClientEx = Assert.Throws<InvalidOperationException>(() => server.AsSamplingChatClient());
|
var asSamplingChatClientEx = Assert.Throws<InvalidOperationException>(() => server.AsSamplingChatClient());
|
||||||
Assert.Equal(expectedSamplingErrorMessage, asSamplingChatClientEx.Message);
|
Assert.Equal(expectedSamplingErrorMessage, asSamplingChatClientEx.Message);
|
||||||
|
|
||||||
var requestSamplingEx = await Assert.ThrowsAsync<InvalidOperationException>(() => server.RequestSamplingAsync([]));
|
var requestSamplingEx = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SampleAsync([]));
|
||||||
Assert.Equal(expectedSamplingErrorMessage, requestSamplingEx.Message);
|
Assert.Equal(expectedSamplingErrorMessage, requestSamplingEx.Message);
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendRequestAsync(new JsonRpcRequest { Method = RequestMethods.SamplingCreateMessage }));
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendRequestAsync(new JsonRpcRequest { Method = RequestMethods.SamplingCreateMessage }));
|
||||||
|
|
@ -206,6 +226,21 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
|
||||||
return ex.Message;
|
return ex.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[McpServerTool(Name = "testElicitationErrors")]
|
||||||
|
public static async Task<string> TestElicitationErrors(IMcpServer server)
|
||||||
|
{
|
||||||
|
const string expectedElicitationErrorMessage = "Elicitation is not supported in stateless mode.";
|
||||||
|
|
||||||
|
// Even when the client has elicitation support, it should not be advertised in stateless mode.
|
||||||
|
Assert.Null(server.ClientCapabilities);
|
||||||
|
|
||||||
|
var requestElicitationEx = Assert.Throws<InvalidOperationException>(() => server.ElicitAsync(new()));
|
||||||
|
Assert.Equal(expectedElicitationErrorMessage, requestElicitationEx.Message);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => server.SendRequestAsync(new JsonRpcRequest { Method = RequestMethods.ElicitationCreate }));
|
||||||
|
return ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
[McpServerTool(Name = "testScope")]
|
[McpServerTool(Name = "testScope")]
|
||||||
public static string? TestScope(ScopedService scopedService) => scopedService.State;
|
public static string? TestScope(ScopedService scopedService) => scopedService.State;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ internal static class Program
|
||||||
{
|
{
|
||||||
throw new McpException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams);
|
throw new McpException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams);
|
||||||
}
|
}
|
||||||
var sampleResult = await request.Server.RequestSamplingAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.GetRawText())),
|
var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.GetRawText())),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
return new CallToolResponse()
|
return new CallToolResponse()
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ public class Program
|
||||||
{
|
{
|
||||||
throw new McpException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams);
|
throw new McpException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams);
|
||||||
}
|
}
|
||||||
var sampleResult = await request.Server.RequestSamplingAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())),
|
var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
return new CallToolResponse()
|
return new CallToolResponse()
|
||||||
|
|
|
||||||
|
|
@ -92,21 +92,21 @@ public class McpServerTests : LoggedTest
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RequestSamplingAsync_Should_Throw_Exception_If_Client_Does_Not_Support_Sampling()
|
public async Task SampleAsync_Should_Throw_Exception_If_Client_Does_Not_Support_Sampling()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
await using var transport = new TestServerTransport();
|
await using var transport = new TestServerTransport();
|
||||||
await using var server = McpServerFactory.Create(transport, _options, LoggerFactory);
|
await using var server = McpServerFactory.Create(transport, _options, LoggerFactory);
|
||||||
SetClientCapabilities(server, new ClientCapabilities());
|
SetClientCapabilities(server, new ClientCapabilities());
|
||||||
|
|
||||||
var action = async () => await server.RequestSamplingAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None);
|
var action = async () => await server.SampleAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(action);
|
await Assert.ThrowsAsync<InvalidOperationException>(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RequestSamplingAsync_Should_SendRequest()
|
public async Task SampleAsync_Should_SendRequest()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
await using var transport = new TestServerTransport();
|
await using var transport = new TestServerTransport();
|
||||||
|
|
@ -116,7 +116,7 @@ public class McpServerTests : LoggedTest
|
||||||
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
|
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await server.RequestSamplingAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None);
|
var result = await server.SampleAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.NotEmpty(transport.SentMessages);
|
Assert.NotEmpty(transport.SentMessages);
|
||||||
|
|
@ -161,6 +161,40 @@ public class McpServerTests : LoggedTest
|
||||||
await runTask;
|
await runTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ElicitAsync_Should_Throw_Exception_If_Client_Does_Not_Support_Elicitation()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var transport = new TestServerTransport();
|
||||||
|
await using var server = McpServerFactory.Create(transport, _options, LoggerFactory);
|
||||||
|
SetClientCapabilities(server, new ClientCapabilities());
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(async () => await server.ElicitAsync(new ElicitRequestParams(), CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ElicitAsync_Should_SendRequest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var transport = new TestServerTransport();
|
||||||
|
await using var server = McpServerFactory.Create(transport, _options, LoggerFactory);
|
||||||
|
SetClientCapabilities(server, new ClientCapabilities { Elicitation = new ElicitationCapability() });
|
||||||
|
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await server.ElicitAsync(new ElicitRequestParams(), CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.NotEmpty(transport.SentMessages);
|
||||||
|
Assert.IsType<JsonRpcRequest>(transport.SentMessages[0]);
|
||||||
|
Assert.Equal(RequestMethods.ElicitationCreate, ((JsonRpcRequest)transport.SentMessages[0]).Method);
|
||||||
|
|
||||||
|
await transport.DisposeAsync();
|
||||||
|
await runTask;
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Handle_Ping_Requests()
|
public async Task Can_Handle_Ping_Requests()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue