Add McpClient/Session methods using .Protocol types (#1063)

This commit is contained in:
Stephen Toub 2025-12-05 13:45:12 -05:00 • committed by GitHub
commit 4a84412331
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1116 additions and 639 deletions

View file

@ -137,12 +137,12 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
Description = options?.Description ?? function.Description,
Arguments = args,
Icons = options?.Icons,
};
// Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available
prompt.Meta = function.UnderlyingMethod is not null ?
AIFunctionMcpServerTool.CreateMetaFromAttributes(function.UnderlyingMethod, options?.Meta, options?.SerializerOptions) :
options?.Meta;
// Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available
Meta = function.UnderlyingMethod is not null ?
AIFunctionMcpServerTool.CreateMetaFromAttributes(function.UnderlyingMethod, options?.Meta) :
options?.Meta
};
return new AIFunctionMcpServerPrompt(function, prompt, options?.Metadata ?? []);
}

View file

@ -221,7 +221,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
MimeType = options?.MimeType ?? "application/octet-stream",
Icons = options?.Icons,
Meta = function.UnderlyingMethod is not null ?
AIFunctionMcpServerTool.CreateMetaFromAttributes(function.UnderlyingMethod, options?.Meta, options?.SerializerOptions) :
AIFunctionMcpServerTool.CreateMetaFromAttributes(function.UnderlyingMethod, options?.Meta) :
options?.Meta,
};

View file

@ -146,7 +146,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
// Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available
tool.Meta = function.UnderlyingMethod is not null ?
CreateMetaFromAttributes(function.UnderlyingMethod, options.Meta, options.SerializerOptions) :
CreateMetaFromAttributes(function.UnderlyingMethod, options.Meta) :
options.Meta;
}
@ -361,9 +361,8 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
/// <summary>Creates a Meta <see cref="JsonObject"/> from <see cref="McpMetaAttribute"/> instances on the specified method.</summary>
/// <param name="method">The method to extract <see cref="McpMetaAttribute"/> instances from.</param>
/// <param name="meta">Optional <see cref="JsonObject"/> to seed the Meta with. Properties from this object take precedence over attributes.</param>
/// <param name="serializerOptions">Optional <see cref="JsonSerializerOptions"/> to use for serialization. This parameter is ignored when parsing JSON strings from attributes.</param>
/// <returns>A <see cref="JsonObject"/> with metadata, or null if no metadata is present.</returns>
internal static JsonObject? CreateMetaFromAttributes(MethodInfo method, JsonObject? meta = null, JsonSerializerOptions? serializerOptions = null)
internal static JsonObject? CreateMetaFromAttributes(MethodInfo method, JsonObject? meta = null)
{
// Transfer all McpMetaAttribute instances to the Meta JsonObject, ignoring any that would overwrite existing properties.
foreach (var attr in method.GetCustomAttributes<McpMetaAttribute>())

View file

@ -13,6 +13,7 @@ public abstract class DelegatingMcpServerPrompt : McpServerPrompt
/// <summary>Initializes a new instance of the <see cref="DelegatingMcpServerPrompt"/> class around the specified <paramref name="innerPrompt"/>.</summary>
/// <param name="innerPrompt">The inner prompt wrapped by this delegating prompt.</param>
/// <exception cref="ArgumentNullException"><paramref name="innerPrompt"/> is <see langword="null"/>.</exception>
protected DelegatingMcpServerPrompt(McpServerPrompt innerPrompt)
{
Throw.IfNull(innerPrompt);

View file

@ -13,6 +13,7 @@ public abstract class DelegatingMcpServerResource : McpServerResource
/// <summary>Initializes a new instance of the <see cref="DelegatingMcpServerResource"/> class around the specified <paramref name="innerResource"/>.</summary>
/// <param name="innerResource">The inner resource wrapped by this delegating resource.</param>
/// <exception cref="ArgumentNullException"><paramref name="innerResource"/> is <see langword="null"/>.</exception>
protected DelegatingMcpServerResource(McpServerResource innerResource)
{
Throw.IfNull(innerResource);

View file

@ -13,6 +13,7 @@ public abstract class DelegatingMcpServerTool : McpServerTool
/// <summary>Initializes a new instance of the <see cref="DelegatingMcpServerTool"/> class around the specified <paramref name="innerTool"/>.</summary>
/// <param name="innerTool">The inner tool wrapped by this delegating tool.</param>
/// <exception cref="ArgumentNullException"><paramref name="innerTool"/> is <see langword="null"/>.</exception>
protected DelegatingMcpServerTool(McpServerTool innerTool)
{
Throw.IfNull(innerTool);

View file

@ -6,7 +6,7 @@ namespace ModelContextProtocol.Server;
/// <typeparam name="TParams">The type of the parameters sent with the request.</typeparam>
/// <typeparam name="TResult">The type of the response returned by the handler.</typeparam>
/// <param name="request">The request context containing the parameters and other metadata.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task representing the asynchronous operation, with the result of the handler.</returns>
public delegate ValueTask<TResult> McpRequestHandler<TParams, TResult>(
RequestContext<TParams> request,

View file

@ -47,18 +47,21 @@ public abstract partial class McpServer : McpSession
/// <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="requestParams">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="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The client does not support sampling.</exception>
public ValueTask<CreateMessageResult> SampleAsync(
CreateMessageRequestParams request, CancellationToken cancellationToken = default)
CreateMessageRequestParams requestParams,
CancellationToken cancellationToken = default)
{
Throw.IfNull(requestParams);
ThrowIfSamplingUnsupported();
return SendRequestAsync(
RequestMethods.SamplingCreateMessage,
request,
requestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult,
cancellationToken: cancellationToken);
@ -157,7 +160,7 @@ public abstract partial class McpServer : McpSession
_ => null,
};
var result = await SampleAsync(new()
var result = await SampleAsync(new CreateMessageRequestParams
{
MaxTokens = chatOptions?.MaxOutputTokens ?? ServerOptions.MaxSamplingOutputTokens,
Messages = samplingMessages,
@ -181,14 +184,16 @@ public abstract partial class McpServer : McpSession
return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContents))
{
ModelId = result.Model,
CreatedAt = DateTimeOffset.UtcNow,
FinishReason = result.StopReason switch
{
CreateMessageResult.StopReasonEndTurn => ChatFinishReason.Stop,
CreateMessageResult.StopReasonMaxTokens => ChatFinishReason.Length,
CreateMessageResult.StopReasonStopSequence => ChatFinishReason.Stop,
CreateMessageResult.StopReasonToolUse => ChatFinishReason.ToolCalls,
CreateMessageResult.StopReasonEndTurn or CreateMessageResult.StopReasonStopSequence => ChatFinishReason.Stop,
_ => null,
}
},
ModelId = result.Model,
};
}
@ -200,31 +205,33 @@ public abstract partial class McpServer : McpSession
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);
}
public ILoggerProvider AsClientLoggerProvider() =>
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="requestParams">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="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The client does not support roots.</exception>
public ValueTask<ListRootsResult> RequestRootsAsync(
ListRootsRequestParams request, CancellationToken cancellationToken = default)
ListRootsRequestParams requestParams,
CancellationToken cancellationToken = default)
{
Throw.IfNull(requestParams);
ThrowIfRootsUnsupported();
return SendRequestAsync(
RequestMethods.RootsList,
request,
requestParams,
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
McpJsonUtilities.JsonContext.Default.ListRootsResult,
cancellationToken: cancellationToken);
@ -233,19 +240,21 @@ public abstract partial class McpServer : McpSession
/// <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="requestParams">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="requestParams"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The client does not support elicitation.</exception>
public ValueTask<ElicitResult> ElicitAsync(
ElicitRequestParams request, CancellationToken cancellationToken = default)
ElicitRequestParams requestParams,
CancellationToken cancellationToken = default)
{
Throw.IfNull(request);
ThrowIfElicitationUnsupported(request);
Throw.IfNull(requestParams);
ThrowIfElicitationUnsupported(requestParams);
return SendRequestAsync(
RequestMethods.ElicitationCreate,
request,
requestParams,
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
McpJsonUtilities.JsonContext.Default.ElicitResult,
cancellationToken: cancellationToken);
@ -257,33 +266,40 @@ public abstract partial class McpServer : McpSession
/// </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="options">Optional request options including metadata, serialization settings, and progress tracking.</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>
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="message"/> is empty or composed entirely of whitespace.</exception>
/// <exception cref="InvalidOperationException">The client does not support elicitation.</exception>
/// <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,
RequestOptions? options = null,
CancellationToken cancellationToken = default)
{
serializerOptions ??= McpJsonUtilities.DefaultOptions;
Throw.IfNullOrWhiteSpace(message);
var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
var dict = s_elicitResultSchemaCache.GetValue(serializerOptions, _ => new());
var schema = dict.GetOrAdd(typeof(T),
#if NET
var schema = dict.GetOrAdd(typeof(T), static (t, s) => BuildRequestSchema(t, s), serializerOptions);
static (t, s) => BuildRequestSchema(t, s), serializerOptions);
#else
var schema = dict.GetOrAdd(typeof(T), type => BuildRequestSchema(type, serializerOptions));
type => BuildRequestSchema(type, serializerOptions));
#endif
var request = new ElicitRequestParams
{
Message = message,
RequestedSchema = schema,
Meta = options?.GetMetaForRequest(),
};
ThrowIfElicitationUnsupported(request);
@ -295,7 +311,7 @@ public abstract partial class McpServer : McpSession
return new ElicitResult<T> { Action = raw.Action, Content = default };
}
var obj = new JsonObject();
JsonObject obj = [];
foreach (var kvp in raw.Content)
{
obj[kvp.Key] = JsonNode.Parse(kvp.Value.GetRawText());

View file

@ -201,7 +201,7 @@ public abstract class McpServerPrompt : IMcpServerPrimitive
/// </param>
/// <param name="options">Optional options used in the creation of the <see cref="McpServerPrompt"/> to control its behavior.</param>
/// <returns>The created <see cref="AIFunction"/> for invoking <paramref name="method"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="method"/> or <paramref name="createTargetFunc"/> is <see langword="null"/>.</exception>
public static McpServerPrompt Create(
MethodInfo method,
Func<RequestContext<GetPromptRequestParams>, object> createTargetFunc,

View file

@ -170,6 +170,7 @@ public abstract class McpServerResource : IMcpServerPrimitive
/// <returns>
/// <see langword="true"/> if the <paramref name="uri"/> matches the <see cref="ProtocolResourceTemplate"/>; otherwise, <see langword="false"/>.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
public abstract bool IsMatch(string uri);
/// <summary>
@ -235,7 +236,7 @@ public abstract class McpServerResource : IMcpServerPrimitive
/// </param>
/// <param name="options">Optional options used in the creation of the <see cref="McpServerResource"/> to control its behavior.</param>
/// <returns>The created <see cref="AIFunction"/> for invoking <paramref name="method"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="method"/> or <paramref name="createTargetFunc"/> is <see langword="null"/>.</exception>
public static McpServerResource Create(
MethodInfo method,
Func<RequestContext<ReadResourceRequestParams>, object> createTargetFunc,

View file

@ -195,7 +195,7 @@ public abstract class McpServerTool : IMcpServerPrimitive
/// </param>
/// <param name="options">Optional options used in the creation of the <see cref="McpServerTool"/> to control its behavior.</param>
/// <returns>The created <see cref="AIFunction"/> for invoking <paramref name="method"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="method"/> or <paramref name="createTargetFunc"/> is <see langword="null"/>.</exception>
public static McpServerTool Create(
MethodInfo method,
Func<RequestContext<CallToolRequestParams>, object> createTargetFunc,

View file

@ -22,6 +22,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>
/// <exception cref="ArgumentNullException"><paramref name="server"/> or <paramref name="jsonRpcRequest"/> is <see langword="null"/>.</exception>
public RequestContext(McpServer server, JsonRpcRequest jsonRpcRequest)
{
Throw.IfNull(server);

View file

@ -76,6 +76,7 @@ public sealed class SseResponseStreamTransport(Stream sseResponseStream, string?
/// <param name="message">The JSON-RPC message received from the client.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task representing the asynchronous operation to buffer the JSON-RPC message for processing.</returns>
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">There is an attempt to process a message before calling <see cref="RunAsync(CancellationToken)"/>.</exception>
/// <remarks>
/// <para>

View file

@ -76,6 +76,11 @@ public sealed class StreamableHttpServerTransport : ITransport
/// <param name="sseResponseStream">The response stream to write MCP JSON-RPC messages as SSE events to.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task representing the send loop that writes JSON-RPC messages to the SSE response stream.</returns>
/// <exception cref="ArgumentNullException"><paramref name="sseResponseStream"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">
/// <see cref="Stateless"/> is <see langword="true"/> and GET requests are not supported in stateless mode,
/// or a GET request has already been started for this session.
/// </exception>
public async Task HandleGetRequestAsync(Stream sseResponseStream, CancellationToken cancellationToken = default)
{
Throw.IfNull(sseResponseStream);
@ -100,13 +105,14 @@ public sealed class StreamableHttpServerTransport : ITransport
/// to the <see cref="JsonRpcRequest"/> that initiated the message.
/// </summary>
/// <param name="message">The JSON-RPC message received from the client via the POST request body.</param>
/// <param name="cancellationToken">This token allows for the operation to be canceled if needed. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="responseStream">The POST response body to write MCP JSON-RPC messages to.</param>
/// <returns>
/// <see langword="true"/> if data was written to the response body.
/// <see false="false"/> if nothing was written because the request body did not contain any <see cref="JsonRpcRequest"/> messages to respond to.
/// The HTTP application should typically respond with an empty "202 Accepted" response in this scenario.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="message"/> or <paramref name="responseStream"/> is <see langword="null"/>.</exception>
/// <remarks>
/// If an authenticated <see cref="ClaimsPrincipal"/> sent the message, that can be included in the <see cref="JsonRpcMessage.Context"/>.
/// No other part of the context should be set.