Add middleware and authz support for server-side handlers (#733)
This commit is contained in:
parent
0f0aad020b
commit
d344c65120
36 changed files with 2871 additions and 435 deletions
|
|
@ -116,14 +116,14 @@ internal sealed partial class McpSession : IDisposable
|
|||
LogMessageRead(EndpointName, message.GetType().Name);
|
||||
|
||||
// Fire and forget the message handling to avoid blocking the transport.
|
||||
if (message.ExecutionContext is null)
|
||||
if (message.Context?.ExecutionContext is null)
|
||||
{
|
||||
_ = ProcessMessageAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Flow the execution context from the HTTP request corresponding to this message if provided.
|
||||
ExecutionContext.Run(message.ExecutionContext, _ => _ = ProcessMessageAsync(), null);
|
||||
ExecutionContext.Run(message.Context.ExecutionContext, _ => _ = ProcessMessageAsync(), null);
|
||||
}
|
||||
|
||||
async Task ProcessMessageAsync()
|
||||
|
|
@ -176,13 +176,15 @@ internal sealed partial class McpSession : IDisposable
|
|||
Message = "An error occurred.",
|
||||
};
|
||||
|
||||
await SendMessageAsync(new JsonRpcError
|
||||
var errorMessage = new JsonRpcError
|
||||
{
|
||||
Id = request.Id,
|
||||
JsonRpc = "2.0",
|
||||
Error = detail,
|
||||
RelatedTransport = request.RelatedTransport,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
Context = new JsonRpcMessageContext { RelatedTransport = request.Context?.RelatedTransport },
|
||||
};
|
||||
|
||||
await SendMessageAsync(errorMessage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (ex is not OperationCanceledException)
|
||||
{
|
||||
|
|
@ -329,7 +331,7 @@ internal sealed partial class McpSession : IDisposable
|
|||
{
|
||||
Id = request.Id,
|
||||
Result = result,
|
||||
RelatedTransport = request.RelatedTransport,
|
||||
Context = request.Context,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
|
|
@ -349,7 +351,7 @@ internal sealed partial class McpSession : IDisposable
|
|||
{
|
||||
Method = NotificationMethods.CancelledNotification,
|
||||
Params = JsonSerializer.SerializeToNode(new CancelledNotificationParams { RequestId = state.Item2.Id }, McpJsonUtilities.JsonContext.Default.CancelledNotificationParams),
|
||||
RelatedTransport = state.Item2.RelatedTransport,
|
||||
Context = new JsonRpcMessageContext { RelatedTransport = state.Item2.Context?.RelatedTransport },
|
||||
});
|
||||
}, Tuple.Create(this, request));
|
||||
}
|
||||
|
|
@ -527,7 +529,7 @@ internal sealed partial class McpSession : IDisposable
|
|||
// Streamable HTTP transport where the specification states that the server SHOULD include JSON-RPC responses in
|
||||
// the HTTP response body for the POST request containing the corresponding JSON-RPC request.
|
||||
private Task SendToRelatedTransportAsync(JsonRpcMessage message, CancellationToken cancellationToken)
|
||||
=> (message.RelatedTransport ?? _transport).SendMessageAsync(message, cancellationToken);
|
||||
=> (message.Context?.RelatedTransport ?? _transport).SendMessageAsync(message, cancellationToken);
|
||||
|
||||
private static CancelledNotificationParams? GetCancelledNotificationParams(JsonNode? notificationParams)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol;
|
|||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When enabled, this capability allows a Model Context Protocol server to provide
|
||||
/// When enabled, this capability allows a Model Context Protocol server to provide
|
||||
/// auto-completion suggestions. This capability is advertised to clients during the initialize handshake.
|
||||
/// </para>
|
||||
/// <para>
|
||||
|
|
@ -33,5 +33,5 @@ public sealed class CompletionsCapability
|
|||
/// and should return appropriate completion suggestions.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<CompleteRequestParams>, CancellationToken, ValueTask<CompleteResult>>? CompleteHandler { get; set; }
|
||||
public McpRequestHandler<CompleteRequestParams, CompleteResult>? CompleteHandler { get; set; }
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using ModelContextProtocol.Server;
|
||||
using System.ComponentModel;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
|
@ -29,28 +30,21 @@ public abstract class JsonRpcMessage
|
|||
public string JsonRpc { get; init; } = "2.0";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport the <see cref="JsonRpcMessage"/> was received on or should be sent over.
|
||||
/// Gets or sets the contextual information for this JSON-RPC message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to support the Streamable HTTP transport where the specification states that the server
|
||||
/// SHOULD include JSON-RPC responses in the HTTP response body for the POST request containing
|
||||
/// the corresponding JSON-RPC request. It may be <see langword="null"/> for other transports.
|
||||
/// This property contains transport-specific and runtime context information that accompanies
|
||||
/// JSON-RPC messages but is not serialized as part of the JSON-RPC payload. This includes
|
||||
/// transport references, execution context, and authenticated user information.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public ITransport? RelatedTransport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="ExecutionContext"/> that should be used to run any handlers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to support the Streamable HTTP transport in its default stateful mode. In this mode,
|
||||
/// the <see cref="IMcpServer"/> outlives the initial HTTP request context it was created on, and new
|
||||
/// JSON-RPC messages can originate from future HTTP requests. This allows the transport to flow the
|
||||
/// context with the JSON-RPC message. This is particularly useful for enabling IHttpContextAccessor
|
||||
/// in tool calls.
|
||||
/// This property should only be set when implementing a custom <see cref="ITransport"/>
|
||||
/// that needs to pass additional per-message context or to pass a <see cref="JsonRpcMessageContext.User"/>
|
||||
/// to <see cref="StreamableHttpServerTransport.HandlePostRequest(JsonRpcMessage, Stream, CancellationToken)"/>
|
||||
/// or <see cref="SseResponseStreamTransport.OnMessageReceivedAsync(JsonRpcMessage, CancellationToken)"/> .
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public ExecutionContext? ExecutionContext { get; set; }
|
||||
public JsonRpcMessageContext? Context { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Provides a <see cref="JsonConverter"/> for <see cref="JsonRpcMessage"/> messages,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
using ModelContextProtocol.Server;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Contains contextual information for JSON-RPC messages that is not part of the JSON-RPC protocol specification.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class holds transport-specific and runtime context information that accompanies JSON-RPC messages
|
||||
/// but is not serialized as part of the JSON-RPC payload. This includes transport references, execution context,
|
||||
/// and authenticated user information.
|
||||
/// </remarks>
|
||||
public class JsonRpcMessageContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the transport the <see cref="JsonRpcMessage"/> was received on or should be sent over.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to support the Streamable HTTP transport where the specification states that the server
|
||||
/// SHOULD include JSON-RPC responses in the HTTP response body for the POST request containing
|
||||
/// the corresponding JSON-RPC request. It may be <see langword="null"/> for other transports.
|
||||
/// </remarks>
|
||||
public ITransport? RelatedTransport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="ExecutionContext"/> that should be used to run any handlers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to support the Streamable HTTP transport in its default stateful mode. In this mode,
|
||||
/// the <see cref="IMcpServer"/> outlives the initial HTTP request context it was created on, and new
|
||||
/// JSON-RPC messages can originate from future HTTP requests. This allows the transport to flow the
|
||||
/// context with the JSON-RPC message. This is particularly useful for enabling IHttpContextAccessor
|
||||
/// in tool calls.
|
||||
/// </remarks>
|
||||
public ExecutionContext? ExecutionContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authenticated user associated with this JSON-RPC message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property contains the <see cref="ClaimsPrincipal"/> representing the authenticated user
|
||||
/// who initiated this JSON-RPC message. This enables request handlers to access user identity
|
||||
/// and authorization information without requiring dependency on HTTP context accessors
|
||||
/// or other HTTP-specific abstractions.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The user information is automatically populated by the transport layer when processing
|
||||
/// incoming HTTP requests in ASP.NET Core scenarios. For other transport types or scenarios
|
||||
/// where user authentication is not applicable, this property may be <see langword="null"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This property is particularly useful in the Streamable HTTP transport where JSON-RPC messages
|
||||
/// may outlive the original HTTP request context, allowing user identity to be preserved
|
||||
/// throughout the message processing pipeline.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ClaimsPrincipal? User { get; set; }
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol;
|
|||
/// <remarks>
|
||||
/// Requests are messages that require a response from the receiver. Each request includes a unique ID
|
||||
/// that will be included in the corresponding response message (either a success response or an error).
|
||||
///
|
||||
///
|
||||
/// The receiver of a request message is expected to execute the specified method with the provided parameters
|
||||
/// and return either a <see cref="JsonRpcResponse"/> with the result, or a <see cref="JsonRpcError"/>
|
||||
/// if the method execution fails.
|
||||
|
|
@ -36,7 +36,7 @@ public sealed class JsonRpcRequest : JsonRpcMessageWithId
|
|||
Id = id,
|
||||
Method = Method,
|
||||
Params = Params,
|
||||
RelatedTransport = RelatedTransport,
|
||||
Context = Context,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,5 @@ public sealed class LoggingCapability
|
|||
/// Gets or sets the handler for set logging level requests from clients.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<SetLevelRequestParams>, CancellationToken, ValueTask<EmptyResult>>? SetLoggingLevelHandler { get; set; }
|
||||
public McpRequestHandler<SetLevelRequestParams, EmptyResult>? SetLoggingLevelHandler { get; set; }
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
|
|
@ -59,4 +60,10 @@ public sealed class Prompt : IBaseMetadata
|
|||
/// </remarks>
|
||||
[JsonPropertyName("_meta")]
|
||||
public JsonObject? Meta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callable server prompt corresponding to this metadata if any.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public McpServerPrompt? McpServerPrompt { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ public sealed class PromptsCapability
|
|||
/// Gets or sets whether this server supports notifications for changes to the prompt list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When set to <see langword="true"/>, the server will send notifications using
|
||||
/// <see cref="NotificationMethods.PromptListChangedNotification"/> when prompts are added,
|
||||
/// When set to <see langword="true"/>, the server will send notifications using
|
||||
/// <see cref="NotificationMethods.PromptListChangedNotification"/> when prompts are added,
|
||||
/// removed, or modified. Clients can register handlers for these notifications to
|
||||
/// refresh their prompt cache. This capability enables clients to stay synchronized with server-side changes
|
||||
/// refresh their prompt cache. This capability enables clients to stay synchronized with server-side changes
|
||||
/// to available prompts.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("listChanged")]
|
||||
|
|
@ -40,15 +40,15 @@ public sealed class PromptsCapability
|
|||
/// along with any prompts defined in <see cref="PromptCollection"/>.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ListPromptsRequestParams>, CancellationToken, ValueTask<ListPromptsResult>>? ListPromptsHandler { get; set; }
|
||||
public McpRequestHandler<ListPromptsRequestParams, ListPromptsResult>? ListPromptsHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for <see cref="RequestMethods.PromptsGet"/> requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This handler is invoked when a client requests details for a specific prompt by name and provides arguments
|
||||
/// for the prompt if needed. The handler receives the request context containing the prompt name and any arguments,
|
||||
/// This handler is invoked when a client requests details for a specific prompt by name and provides arguments
|
||||
/// for the prompt if needed. The handler receives the request context containing the prompt name and any arguments,
|
||||
/// and should return a <see cref="GetPromptResult"/> with the prompt messages and other details.
|
||||
/// </para>
|
||||
/// <para>
|
||||
|
|
@ -57,7 +57,7 @@ public sealed class PromptsCapability
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<GetPromptRequestParams>, CancellationToken, ValueTask<GetPromptResult>>? GetPromptHandler { get; set; }
|
||||
public McpRequestHandler<GetPromptRequestParams, GetPromptResult>? GetPromptHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of prompts that will be served by the server.
|
||||
|
|
@ -69,7 +69,7 @@ public sealed class PromptsCapability
|
|||
/// when those are provided:
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// - For <see cref="RequestMethods.PromptsList"/> requests: The server returns all prompts from this collection
|
||||
/// - For <see cref="RequestMethods.PromptsList"/> requests: The server returns all prompts from this collection
|
||||
/// plus any additional prompts provided by the <see cref="ListPromptsHandler"/> if it's set.
|
||||
/// </para>
|
||||
/// <para>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
|
|
@ -87,4 +88,10 @@ public sealed class Resource : IBaseMetadata
|
|||
/// </remarks>
|
||||
[JsonPropertyName("_meta")]
|
||||
public JsonObject? Meta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callable server resource corresponding to this metadata if any.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public McpServerResource? McpServerResource { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
|
|
@ -84,6 +85,12 @@ public sealed class ResourceTemplate : IBaseMetadata
|
|||
[JsonIgnore]
|
||||
public bool IsTemplated => UriTemplate.Contains('{');
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callable server resource corresponding to this metadata if any.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public McpServerResource? McpServerResource { get; set; }
|
||||
|
||||
/// <summary>Converts the <see cref="ResourceTemplate"/> into a <see cref="Resource"/>.</summary>
|
||||
/// <returns>A <see cref="Resource"/> if <see cref="IsTemplated"/> is <see langword="false"/>; otherwise, <see langword="null"/>.</returns>
|
||||
public Resource? AsResource()
|
||||
|
|
@ -102,6 +109,7 @@ public sealed class ResourceTemplate : IBaseMetadata
|
|||
MimeType = MimeType,
|
||||
Annotations = Annotations,
|
||||
Meta = Meta,
|
||||
McpServerResource = McpServerResource,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -21,8 +21,8 @@ public sealed class ResourcesCapability
|
|||
/// Gets or sets whether this server supports notifications for changes to the resource list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When set to <see langword="true"/>, the server will send notifications using
|
||||
/// <see cref="NotificationMethods.ResourceListChangedNotification"/> when resources are added,
|
||||
/// When set to <see langword="true"/>, the server will send notifications using
|
||||
/// <see cref="NotificationMethods.ResourceListChangedNotification"/> when resources are added,
|
||||
/// removed, or modified. Clients can register handlers for these notifications to
|
||||
/// refresh their resource cache.
|
||||
/// </remarks>
|
||||
|
|
@ -39,7 +39,7 @@ public sealed class ResourcesCapability
|
|||
/// allowing clients to discover available resource types and their access patterns.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ListResourceTemplatesRequestParams>, CancellationToken, ValueTask<ListResourceTemplatesResult>>? ListResourceTemplatesHandler { get; set; }
|
||||
public McpRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>? ListResourceTemplatesHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesList"/> requests.
|
||||
|
|
@ -49,7 +49,7 @@ public sealed class ResourcesCapability
|
|||
/// The implementation should return a <see cref="ListResourcesResult"/> with the matching resources.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ListResourcesRequestParams>, CancellationToken, ValueTask<ListResourcesResult>>? ListResourcesHandler { get; set; }
|
||||
public McpRequestHandler<ListResourcesRequestParams, ListResourcesResult>? ListResourcesHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesRead"/> requests.
|
||||
|
|
@ -61,7 +61,7 @@ public sealed class ResourcesCapability
|
|||
/// its contents in a ReadResourceResult object.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ReadResourceRequestParams>, CancellationToken, ValueTask<ReadResourceResult>>? ReadResourceHandler { get; set; }
|
||||
public McpRequestHandler<ReadResourceRequestParams, ReadResourceResult>? ReadResourceHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesSubscribe"/> requests.
|
||||
|
|
@ -74,7 +74,7 @@ public sealed class ResourcesCapability
|
|||
/// requiring polling.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<SubscribeRequestParams>, CancellationToken, ValueTask<EmptyResult>>? SubscribeToResourcesHandler { get; set; }
|
||||
public McpRequestHandler<SubscribeRequestParams, EmptyResult>? SubscribeToResourcesHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesUnsubscribe"/> requests.
|
||||
|
|
@ -85,7 +85,7 @@ public sealed class ResourcesCapability
|
|||
/// about the specified resource.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<UnsubscribeRequestParams>, CancellationToken, ValueTask<EmptyResult>>? UnsubscribeFromResourcesHandler { get; set; }
|
||||
public McpRequestHandler<UnsubscribeRequestParams, EmptyResult>? UnsubscribeFromResourcesHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of resources served by the server.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ public sealed class Tool : IBaseMetadata
|
|||
/// if an invalid schema is provided.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The schema typically defines the properties (parameters) that the tool accepts,
|
||||
/// The schema typically defines the properties (parameters) that the tool accepts,
|
||||
/// their types, and which ones are required. This helps AI models understand
|
||||
/// how to structure their calls to the tool.
|
||||
/// </para>
|
||||
|
|
@ -52,9 +53,9 @@ public sealed class Tool : IBaseMetadata
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonPropertyName("inputSchema")]
|
||||
public JsonElement InputSchema
|
||||
{
|
||||
get => field;
|
||||
public JsonElement InputSchema
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (!McpJsonUtilities.IsValidMcpToolSchema(value))
|
||||
|
|
@ -114,4 +115,10 @@ public sealed class Tool : IBaseMetadata
|
|||
/// </remarks>
|
||||
[JsonPropertyName("_meta")]
|
||||
public JsonObject? Meta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callable server tool corresponding to this metadata if any.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public McpServerTool? McpServerTool { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ public sealed class ToolsCapability
|
|||
/// Gets or sets whether this server supports notifications for changes to the tool list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When set to <see langword="true"/>, the server will send notifications using
|
||||
/// <see cref="NotificationMethods.ToolListChangedNotification"/> when tools are added,
|
||||
/// When set to <see langword="true"/>, the server will send notifications using
|
||||
/// <see cref="NotificationMethods.ToolListChangedNotification"/> when tools are added,
|
||||
/// removed, or modified. Clients can register handlers for these notifications to
|
||||
/// refresh their tool cache. This capability enables clients to stay synchronized with server-side
|
||||
/// refresh their tool cache. This capability enables clients to stay synchronized with server-side
|
||||
/// changes to available tools.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("listChanged")]
|
||||
|
|
@ -33,19 +33,19 @@ public sealed class ToolsCapability
|
|||
/// and the tools from the collection will be combined to form the complete list of available tools.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ListToolsRequestParams>, CancellationToken, ValueTask<ListToolsResult>>? ListToolsHandler { get; set; }
|
||||
public McpRequestHandler<ListToolsRequestParams, ListToolsResult>? ListToolsHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for <see cref="RequestMethods.ToolsCall"/> requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This handler is invoked when a client makes a call to a tool that isn't found in the <see cref="ToolCollection"/>.
|
||||
/// The handler should implement logic to execute the requested tool and return appropriate results.
|
||||
/// It receives a <see cref="RequestContext{CallToolRequestParams}"/> containing information about the tool
|
||||
/// The handler should implement logic to execute the requested tool and return appropriate results.
|
||||
/// It receives a <see cref="RequestContext{CallToolRequestParams}"/> containing information about the tool
|
||||
/// being called and its arguments, and should return a <see cref="CallToolResult"/> with the execution results.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<CallToolRequestParams>, CancellationToken, ValueTask<CallToolResult>>? CallToolHandler { get; set; }
|
||||
public McpRequestHandler<CallToolRequestParams, CallToolResult>? CallToolHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of tools served by the server.
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ internal sealed class RequestHandlers : Dictionary<string, Func<JsonRpcRequest,
|
|||
/// <summary>
|
||||
/// Registers a handler for incoming requests of a specific method in the MCP protocol.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">Type of request payload that will be deserialized from incoming JSON</typeparam>
|
||||
/// <typeparam name="TResponse">Type of response payload that will be serialized to JSON (not full RPC response)</typeparam>
|
||||
/// <typeparam name="TParams">Type of request payload that will be deserialized from incoming JSON</typeparam>
|
||||
/// <typeparam name="TResult">Type of response payload that will be serialized to JSON (not full RPC response)</typeparam>
|
||||
/// <param name="method">Method identifier to register for (e.g., "tools/list", "logging/setLevel")</param>
|
||||
/// <param name="handler">Handler function to be called when a request with the specified method identifier is received</param>
|
||||
/// <param name="requestTypeInfo">The JSON contract governing request parameter deserialization</param>
|
||||
|
|
@ -23,15 +23,15 @@ internal sealed class RequestHandlers : Dictionary<string, Func<JsonRpcRequest,
|
|||
/// deserialized request parameters.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The handler function receives the deserialized request object and a cancellation token, and should return
|
||||
/// a response object that will be serialized back to the client.
|
||||
/// The handler function receives the deserialized request object, the full JSON-RPC request, and a cancellation token,
|
||||
/// and should return a response object that will be serialized back to the client.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public void Set<TRequest, TResponse>(
|
||||
public void Set<TParams, TResult>(
|
||||
string method,
|
||||
Func<TRequest?, ITransport?, CancellationToken, ValueTask<TResponse>> handler,
|
||||
JsonTypeInfo<TRequest> requestTypeInfo,
|
||||
JsonTypeInfo<TResponse> responseTypeInfo)
|
||||
Func<TParams?, JsonRpcRequest, CancellationToken, ValueTask<TResult>> handler,
|
||||
JsonTypeInfo<TParams> requestTypeInfo,
|
||||
JsonTypeInfo<TResult> responseTypeInfo)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
Throw.IfNull(handler);
|
||||
|
|
@ -40,8 +40,8 @@ internal sealed class RequestHandlers : Dictionary<string, Func<JsonRpcRequest,
|
|||
|
||||
this[method] = async (request, cancellationToken) =>
|
||||
{
|
||||
TRequest? typedRequest = JsonSerializer.Deserialize(request.Params, requestTypeInfo);
|
||||
object? result = await handler(typedRequest, request.RelatedTransport, cancellationToken).ConfigureAwait(false);
|
||||
TParams? typedRequest = JsonSerializer.Deserialize(request.Params, requestTypeInfo);
|
||||
object? result = await handler(typedRequest, request, cancellationToken).ConfigureAwait(false);
|
||||
return JsonSerializer.SerializeToNode(result, responseTypeInfo);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ namespace ModelContextProtocol.Server;
|
|||
/// <summary>Provides an <see cref="McpServerPrompt"/> that's implemented via an <see cref="AIFunction"/>.</summary>
|
||||
internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
||||
{
|
||||
private readonly IReadOnlyList<object> _metadata;
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerPrompt"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
/// </summary>
|
||||
|
|
@ -136,7 +137,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
Arguments = args,
|
||||
};
|
||||
|
||||
return new AIFunctionMcpServerPrompt(function, prompt);
|
||||
return new AIFunctionMcpServerPrompt(function, prompt, options?.Metadata ?? []);
|
||||
}
|
||||
|
||||
private static McpServerPromptCreateOptions DeriveOptions(MethodInfo method, McpServerPromptCreateOptions? options)
|
||||
|
|
@ -154,6 +155,9 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
newOptions.Description ??= descAttr.Description;
|
||||
}
|
||||
|
||||
// Set metadata if not already provided
|
||||
newOptions.Metadata ??= AIFunctionMcpServerTool.CreateMetadata(method);
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
|
|
@ -161,15 +165,20 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
internal AIFunction AIFunction { get; }
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="McpServerPrompt"/> class.</summary>
|
||||
private AIFunctionMcpServerPrompt(AIFunction function, Prompt prompt)
|
||||
private AIFunctionMcpServerPrompt(AIFunction function, Prompt prompt, IReadOnlyList<object> metadata)
|
||||
{
|
||||
AIFunction = function;
|
||||
ProtocolPrompt = prompt;
|
||||
ProtocolPrompt.McpServerPrompt = this;
|
||||
_metadata = metadata;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Prompt ProtocolPrompt { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<object> Metadata => _metadata;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<GetPromptResult> GetAsync(
|
||||
RequestContext<GetPromptRequestParams> request, CancellationToken cancellationToken = default)
|
||||
|
|
@ -177,7 +186,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
|||
Throw.IfNull(request);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
request.Services = new RequestServiceProvider<GetPromptRequestParams>(request, request.Services);
|
||||
request.Services = new RequestServiceProvider<GetPromptRequestParams>(request);
|
||||
AIFunctionArguments arguments = new() { Services = request.Services };
|
||||
|
||||
if (request.Params?.Arguments is { } argDict)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.ComponentModel;
|
|||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
|
@ -17,6 +18,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
{
|
||||
private readonly Regex? _uriParser;
|
||||
private readonly string[] _templateVariableNames = [];
|
||||
private readonly IReadOnlyList<object> _metadata;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AIFunctionMcpServerResource"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
|
|
@ -218,7 +220,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
MimeType = options?.MimeType ?? "application/octet-stream",
|
||||
};
|
||||
|
||||
return new AIFunctionMcpServerResource(function, resource);
|
||||
return new AIFunctionMcpServerResource(function, resource, options?.Metadata ?? []);
|
||||
}
|
||||
|
||||
private static McpServerResourceCreateOptions DeriveOptions(MemberInfo member, McpServerResourceCreateOptions? options)
|
||||
|
|
@ -238,6 +240,12 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
newOptions.Description ??= descAttr.Description;
|
||||
}
|
||||
|
||||
// Set metadata if not already provided and the member is a MethodInfo
|
||||
if (member is MethodInfo method)
|
||||
{
|
||||
newOptions.Metadata ??= AIFunctionMcpServerTool.CreateMetadata(method);
|
||||
}
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
|
|
@ -270,11 +278,13 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
internal AIFunction AIFunction { get; }
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="McpServerResource"/> class.</summary>
|
||||
private AIFunctionMcpServerResource(AIFunction function, ResourceTemplate resourceTemplate)
|
||||
private AIFunctionMcpServerResource(AIFunction function, ResourceTemplate resourceTemplate, IReadOnlyList<object> metadata)
|
||||
{
|
||||
AIFunction = function;
|
||||
ProtocolResourceTemplate = resourceTemplate;
|
||||
ProtocolResourceTemplate.McpServerResource = this;
|
||||
ProtocolResource = resourceTemplate.AsResource();
|
||||
_metadata = metadata;
|
||||
|
||||
if (ProtocolResource is null)
|
||||
{
|
||||
|
|
@ -289,6 +299,9 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
/// <inheritdoc />
|
||||
public override Resource? ProtocolResource { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<object> Metadata => _metadata;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<ReadResourceResult?> ReadAsync(
|
||||
RequestContext<ReadResourceRequestParams> request, CancellationToken cancellationToken = default)
|
||||
|
|
@ -316,7 +329,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
|||
}
|
||||
|
||||
// Build up the arguments for the AIFunction call, including all of the name/value pairs from the URI.
|
||||
request.Services = new RequestServiceProvider<ReadResourceRequestParams>(request, request.Services);
|
||||
request.Services = new RequestServiceProvider<ReadResourceRequestParams>(request);
|
||||
AIFunctionArguments arguments = new() { Services = request.Services };
|
||||
|
||||
// For templates, populate the arguments from the URI template.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
|
|
@ -15,8 +13,8 @@ namespace ModelContextProtocol.Server;
|
|||
/// <summary>Provides an <see cref="McpServerTool"/> that's implemented via an <see cref="AIFunction"/>.</summary>
|
||||
internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly bool _structuredOutputRequiresWrapping;
|
||||
private readonly IReadOnlyList<object> _metadata;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
|
|
@ -26,7 +24,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
McpServerToolCreateOptions? options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
|
||||
options = DeriveOptions(method.Method, options);
|
||||
|
||||
return Create(method.Method, method.Target, options);
|
||||
|
|
@ -146,7 +144,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
}
|
||||
}
|
||||
|
||||
return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping);
|
||||
return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping, options?.Metadata ?? []);
|
||||
}
|
||||
|
||||
private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options)
|
||||
|
|
@ -186,6 +184,9 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
newOptions.Description ??= descAttr.Description;
|
||||
}
|
||||
|
||||
// Set metadata if not already provided
|
||||
newOptions.Metadata ??= CreateMetadata(method);
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
|
|
@ -193,17 +194,22 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
internal AIFunction AIFunction { get; }
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="McpServerTool"/> class.</summary>
|
||||
private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider, bool structuredOutputRequiresWrapping)
|
||||
private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider, bool structuredOutputRequiresWrapping, IReadOnlyList<object> metadata)
|
||||
{
|
||||
AIFunction = function;
|
||||
ProtocolTool = tool;
|
||||
_logger = serviceProvider?.GetService<ILoggerFactory>()?.CreateLogger<McpServerTool>() ?? (ILogger)NullLogger.Instance;
|
||||
ProtocolTool.McpServerTool = this;
|
||||
|
||||
_structuredOutputRequiresWrapping = structuredOutputRequiresWrapping;
|
||||
_metadata = metadata;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Tool ProtocolTool { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<object> Metadata => _metadata;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<CallToolResult> InvokeAsync(
|
||||
RequestContext<CallToolRequestParams> request, CancellationToken cancellationToken = default)
|
||||
|
|
@ -211,7 +217,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
Throw.IfNull(request);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
request.Services = new RequestServiceProvider<CallToolRequestParams>(request, request.Services);
|
||||
request.Services = new RequestServiceProvider<CallToolRequestParams>(request);
|
||||
AIFunctionArguments arguments = new() { Services = request.Services };
|
||||
|
||||
if (request.Params?.Arguments is { } argDict)
|
||||
|
|
@ -223,24 +229,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
}
|
||||
|
||||
object? result;
|
||||
try
|
||||
{
|
||||
result = await AIFunction.InvokeAsync(arguments, 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 }],
|
||||
};
|
||||
}
|
||||
result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
JsonNode? structuredContent = CreateStructuredResponse(result);
|
||||
return result switch
|
||||
|
|
@ -257,27 +246,27 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
Content = [],
|
||||
StructuredContent = structuredContent,
|
||||
},
|
||||
|
||||
|
||||
string text => new()
|
||||
{
|
||||
Content = [new TextContentBlock { Text = text }],
|
||||
StructuredContent = structuredContent,
|
||||
},
|
||||
|
||||
|
||||
ContentBlock content => new()
|
||||
{
|
||||
Content = [content],
|
||||
StructuredContent = structuredContent,
|
||||
},
|
||||
|
||||
|
||||
IEnumerable<AIContent> contentItems => ConvertAIContentEnumerableToCallToolResult(contentItems, structuredContent),
|
||||
|
||||
|
||||
IEnumerable<ContentBlock> contents => new()
|
||||
{
|
||||
Content = [.. contents],
|
||||
StructuredContent = structuredContent,
|
||||
},
|
||||
|
||||
|
||||
CallToolResult callToolResponse => callToolResponse,
|
||||
|
||||
_ => new()
|
||||
|
|
@ -336,6 +325,26 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates metadata from attributes on the specified method and its declaring class, with the MethodInfo as the first item.</summary>
|
||||
internal static IReadOnlyList<object> CreateMetadata(MethodInfo method)
|
||||
{
|
||||
// Add the MethodInfo to the start of the metadata similar to what RouteEndpointDataSource does for minimal endpoints.
|
||||
List<object> metadata = [method];
|
||||
|
||||
// Add class-level attributes first, since those are less specific.
|
||||
if (method.DeclaringType is not null)
|
||||
{
|
||||
metadata.AddRange(method.DeclaringType.GetCustomAttributes());
|
||||
}
|
||||
|
||||
// Add method-level attributes second, since those are more specific.
|
||||
// When metadata conflicts, later metadata usually takes precedence with exceptions for metadata like
|
||||
// IAllowAnonymous which always take precedence over IAuthorizeData no matter the order.
|
||||
metadata.AddRange(method.GetCustomAttributes());
|
||||
|
||||
return metadata.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>Regex that flags runs of characters other than ASCII digits or letters.</summary>
|
||||
#if NET
|
||||
[GeneratedRegex("[^0-9A-Za-z]+")]
|
||||
|
|
@ -446,7 +455,4 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
|
|||
IsError = allErrorContent && hasAny
|
||||
};
|
||||
}
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "\"{ToolName}\" threw an unhandled exception.")]
|
||||
private partial void ToolCallError(string toolName, Exception exception);
|
||||
}
|
||||
|
|
@ -22,15 +22,25 @@ internal sealed class DestinationBoundMcpServer(McpServer server, ITransport? tr
|
|||
|
||||
public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Debug.Assert(message.RelatedTransport is null);
|
||||
message.RelatedTransport = transport;
|
||||
if (message.Context is not null)
|
||||
{
|
||||
throw new ArgumentException("Only transports can provide a JsonRpcMessageContext.");
|
||||
}
|
||||
|
||||
message.Context = new JsonRpcMessageContext();
|
||||
message.Context.RelatedTransport = transport;
|
||||
return server.SendMessageAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Debug.Assert(request.RelatedTransport is null);
|
||||
request.RelatedTransport = transport;
|
||||
if (request.Context is not null)
|
||||
{
|
||||
throw new ArgumentException("Only transports can provide a JsonRpcMessageContext.");
|
||||
}
|
||||
|
||||
request.Context = new JsonRpcMessageContext();
|
||||
request.Context.RelatedTransport = transport;
|
||||
return server.SendRequestAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,13 @@ public interface IMcpServerPrimitive
|
|||
{
|
||||
/// <summary>Gets the unique identifier of the primitive.</summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata for this primitive instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Contains attributes from the associated MethodInfo and declaring class (if any),
|
||||
/// with class-level attributes appearing before method-level attributes.
|
||||
/// </remarks>
|
||||
IReadOnlyList<object> Metadata { get; }
|
||||
}
|
||||
|
|
|
|||
11
src/ModelContextProtocol.Core/Server/McpRequestFilter.cs
Normal file
11
src/ModelContextProtocol.Core/Server/McpRequestFilter.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace ModelContextProtocol.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate type for applying filters to incoming MCP requests with specific parameter and result types.
|
||||
/// </summary>
|
||||
/// <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="next">The next request handler in the pipeline.</param>
|
||||
/// <returns>The next request handler wrapped with the filter.</returns>
|
||||
public delegate McpRequestHandler<TParams, TResult> McpRequestFilter<TParams, TResult>(
|
||||
McpRequestHandler<TParams, TResult> next);
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue