Add middleware and authz support for server-side handlers (#733)

This commit is contained in:
Stephen Halter 2025-09-10 10:07:45 -07:00 • committed by GitHub
commit d344c65120
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 2871 additions and 435 deletions

View file

@ -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)

View file

@ -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.

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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; }
}

View 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);

View file

@ -0,0 +1,13 @@
namespace ModelContextProtocol.Server;
/// <summary>
/// Delegate type for handling 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="request">The request context containing the parameters and other metadata.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</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,
CancellationToken cancellationToken);

View file

@ -7,7 +7,7 @@ using System.Text.Json.Serialization.Metadata;
namespace ModelContextProtocol.Server;
/// <inheritdoc />
internal sealed class McpServer : McpEndpoint, IMcpServer
internal sealed partial class McpServer : McpEndpoint, IMcpServer
{
internal static Implementation DefaultImplementation { get; } = new()
{
@ -195,9 +195,12 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
return;
}
var completeHandler = completionsCapability.CompleteHandler ?? (static async (_, __) => new CompleteResult());
completeHandler = BuildFilterPipeline(completeHandler, options.Filters.CompleteFilters);
ServerCapabilities.Completions = new()
{
CompleteHandler = completionsCapability.CompleteHandler ?? (static async (_, __) => new CompleteResult())
CompleteHandler = completeHandler
};
SetHandler(
@ -279,30 +282,14 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
var originalReadResourceHandler = readResourceHandler;
readResourceHandler = async (request, cancellationToken) =>
{
if (request.Params?.Uri is string uri)
if (request.MatchedPrimitive is McpServerResource matchedResource)
{
// First try an O(1) lookup by exact match.
if (resources.TryGetPrimitive(uri, out var resource))
if (await matchedResource.ReadAsync(request, cancellationToken).ConfigureAwait(false) is { } result)
{
if (await resource.ReadAsync(request, cancellationToken).ConfigureAwait(false) is { } result)
{
return result;
}
}
// Fall back to an O(N) lookup, trying to match against each URI template.
// The number of templates is controlled by the server developer, and the number is expected to be
// not terribly large. If that changes, this can be tweaked to enable a more efficient lookup.
foreach (var resourceTemplate in resources)
{
if (await resourceTemplate.ReadAsync(request, cancellationToken).ConfigureAwait(false) is { } result)
{
return result;
}
return result;
}
}
// Finally fall back to the handler.
return await originalReadResourceHandler(request, cancellationToken).ConfigureAwait(false);
};
@ -312,6 +299,43 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
// subscribe = true;
}
listResourcesHandler = BuildFilterPipeline(listResourcesHandler, options.Filters.ListResourcesFilters);
listResourceTemplatesHandler = BuildFilterPipeline(listResourceTemplatesHandler, options.Filters.ListResourceTemplatesFilters);
readResourceHandler = BuildFilterPipeline(readResourceHandler, options.Filters.ReadResourceFilters, handler =>
async (request, cancellationToken) =>
{
// Initial handler that sets MatchedPrimitive
if (request.Params?.Uri is { } uri && resources is not null)
{
// First try an O(1) lookup by exact match.
if (resources.TryGetPrimitive(uri, out var resource))
{
request.MatchedPrimitive = resource;
}
else
{
// Fall back to an O(N) lookup, trying to match against each URI template.
// The number of templates is controlled by the server developer, and the number is expected to be
// not terribly large. If that changes, this can be tweaked to enable a more efficient lookup.
foreach (var resourceTemplate in resources)
{
// Check if this template would handle the request by testing if ReadAsync would succeed
if (resourceTemplate.IsTemplated)
{
// This is a simplified check - a more robust implementation would match the URI pattern
// For now, we'll let the actual handler attempt the match
request.MatchedPrimitive = resourceTemplate;
break;
}
}
}
}
return await handler(request, cancellationToken).ConfigureAwait(false);
});
subscribeHandler = BuildFilterPipeline(subscribeHandler, options.Filters.SubscribeToResourcesFilters);
unsubscribeHandler = BuildFilterPipeline(unsubscribeHandler, options.Filters.UnsubscribeFromResourcesFilters);
ServerCapabilities.Resources.ListResourcesHandler = listResourcesHandler;
ServerCapabilities.Resources.ListResourceTemplatesHandler = listResourceTemplatesHandler;
ServerCapabilities.Resources.ReadResourceHandler = readResourceHandler;
@ -390,8 +414,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
var originalGetPromptHandler = getPromptHandler;
getPromptHandler = (request, cancellationToken) =>
{
if (request.Params is not null &&
prompts.TryGetPrimitive(request.Params.Name, out var prompt))
if (request.MatchedPrimitive is McpServerPrompt prompt)
{
return prompt.GetAsync(request, cancellationToken);
}
@ -402,6 +425,20 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
listChanged = true;
}
listPromptsHandler = BuildFilterPipeline(listPromptsHandler, options.Filters.ListPromptsFilters);
getPromptHandler = BuildFilterPipeline(getPromptHandler, options.Filters.GetPromptFilters, handler =>
(request, cancellationToken) =>
{
// Initial handler that sets MatchedPrimitive
if (request.Params?.Name is { } promptName && prompts is not null &&
prompts.TryGetPrimitive(promptName, out var prompt))
{
request.MatchedPrimitive = prompt;
}
return handler(request, cancellationToken);
});
ServerCapabilities.Prompts.ListPromptsHandler = listPromptsHandler;
ServerCapabilities.Prompts.GetPromptHandler = getPromptHandler;
ServerCapabilities.Prompts.PromptCollection = prompts;
@ -458,8 +495,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
var originalCallToolHandler = callToolHandler;
callToolHandler = (request, cancellationToken) =>
{
if (request.Params is not null &&
tools.TryGetPrimitive(request.Params.Name, out var tool))
if (request.MatchedPrimitive is McpServerTool tool)
{
return tool.InvokeAsync(request, cancellationToken);
}
@ -470,6 +506,51 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
listChanged = true;
}
listToolsHandler = BuildFilterPipeline(listToolsHandler, options.Filters.ListToolsFilters);
callToolHandler = BuildFilterPipeline(callToolHandler, options.Filters.CallToolFilters, handler =>
(request, cancellationToken) =>
{
// Initial handler that sets MatchedPrimitive
if (request.Params?.Name is { } toolName && tools is not null &&
tools.TryGetPrimitive(toolName, out var tool))
{
request.MatchedPrimitive = tool;
}
return handler(request, cancellationToken);
}, handler =>
async (request, cancellationToken) =>
{
// Final handler that provides exception handling only for tool execution
// Only wrap tool execution in try-catch, not tool resolution
if (request.MatchedPrimitive is McpServerTool)
{
try
{
return await handler(request, cancellationToken).ConfigureAwait(false);
}
catch (Exception e) when (e is not OperationCanceledException)
{
ToolCallError(request.Params?.Name ?? string.Empty, e);
string errorMessage = e is McpException ?
$"An error occurred invoking '{request.Params?.Name}': {e.Message}" :
$"An error occurred invoking '{request.Params?.Name}'.";
return new()
{
IsError = true,
Content = [new TextContentBlock { Text = errorMessage }],
};
}
}
else
{
// For unmatched tools, let exceptions bubble up as protocol errors
return await handler(request, cancellationToken).ConfigureAwait(false);
}
});
ServerCapabilities.Tools.ListToolsHandler = listToolsHandler;
ServerCapabilities.Tools.CallToolHandler = callToolHandler;
ServerCapabilities.Tools.ToolCollection = tools;
@ -493,12 +574,18 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
// We don't require that the handler be provided, as we always store the provided log level to the server.
var setLoggingLevelHandler = options.Capabilities?.Logging?.SetLoggingLevelHandler;
// Apply filters to the handler
if (setLoggingLevelHandler is not null)
{
setLoggingLevelHandler = BuildFilterPipeline(setLoggingLevelHandler, options.Filters.SetLoggingLevelFilters);
}
ServerCapabilities.Logging = new();
ServerCapabilities.Logging.SetLoggingLevelHandler = setLoggingLevelHandler;
RequestHandlers.Set(
RequestMethods.LoggingSetLevel,
(request, destinationTransport, cancellationToken) =>
(request, jsonRpcRequest, cancellationToken) =>
{
// Store the provided level.
if (request is not null)
@ -514,7 +601,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
// If a handler was provided, now delegate to it.
if (setLoggingLevelHandler is not null)
{
return InvokeHandlerAsync(setLoggingLevelHandler, request, destinationTransport, cancellationToken);
return InvokeHandlerAsync(setLoggingLevelHandler, request, jsonRpcRequest, cancellationToken);
}
// Otherwise, consider it handled.
@ -525,25 +612,26 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
}
private ValueTask<TResult> InvokeHandlerAsync<TParams, TResult>(
Func<RequestContext<TParams>, CancellationToken, ValueTask<TResult>> handler,
McpRequestHandler<TParams, TResult> handler,
TParams? args,
ITransport? destinationTransport = null,
JsonRpcRequest jsonRpcRequest,
CancellationToken cancellationToken = default)
{
return _servicesScopePerRequest ?
InvokeScopedAsync(handler, args, cancellationToken) :
handler(new(new DestinationBoundMcpServer(this, destinationTransport)) { Params = args }, cancellationToken);
InvokeScopedAsync(handler, args, jsonRpcRequest, cancellationToken) :
handler(new(new DestinationBoundMcpServer(this, jsonRpcRequest.Context?.RelatedTransport), jsonRpcRequest) { Params = args }, cancellationToken);
async ValueTask<TResult> InvokeScopedAsync(
Func<RequestContext<TParams>, CancellationToken, ValueTask<TResult>> handler,
McpRequestHandler<TParams, TResult> handler,
TParams? args,
JsonRpcRequest jsonRpcRequest,
CancellationToken cancellationToken)
{
var scope = Services?.GetService<IServiceScopeFactory>()?.CreateAsyncScope();
try
{
return await handler(
new RequestContext<TParams>(new DestinationBoundMcpServer(this, destinationTransport))
new RequestContext<TParams>(new DestinationBoundMcpServer(this, jsonRpcRequest.Context?.RelatedTransport), jsonRpcRequest)
{
Services = scope?.ServiceProvider ?? Services,
Params = args
@ -560,18 +648,44 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
}
}
private void SetHandler<TRequest, TResponse>(
private void SetHandler<TParams, TResult>(
string method,
Func<RequestContext<TRequest>, CancellationToken, ValueTask<TResponse>> handler,
JsonTypeInfo<TRequest> requestTypeInfo,
JsonTypeInfo<TResponse> responseTypeInfo)
McpRequestHandler<TParams, TResult> handler,
JsonTypeInfo<TParams> requestTypeInfo,
JsonTypeInfo<TResult> responseTypeInfo)
{
RequestHandlers.Set(method,
(request, destinationTransport, cancellationToken) =>
InvokeHandlerAsync(handler, request, destinationTransport, cancellationToken),
RequestHandlers.Set(method,
(request, jsonRpcRequest, cancellationToken) =>
InvokeHandlerAsync(handler, request, jsonRpcRequest, cancellationToken),
requestTypeInfo, responseTypeInfo);
}
private static McpRequestHandler<TParams, TResult> BuildFilterPipeline<TParams, TResult>(
McpRequestHandler<TParams, TResult> baseHandler,
List<McpRequestFilter<TParams, TResult>> filters,
McpRequestFilter<TParams, TResult>? initialHandler = null,
McpRequestFilter<TParams, TResult>? finalHandler = null)
{
var current = baseHandler;
if (finalHandler is not null)
{
current = finalHandler(current);
}
for (int i = filters.Count - 1; i >= 0; i--)
{
current = filters[i](current);
}
if (initialHandler is not null)
{
current = initialHandler(current);
}
return current;
}
private void UpdateEndpointNameWithClientInfo()
{
if (ClientInfo is null)
@ -594,4 +708,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
LogLevel.Critical => Protocol.LoggingLevel.Critical,
_ => Protocol.LoggingLevel.Emergency,
};
[LoggerMessage(Level = LogLevel.Error, Message = "\"{ToolName}\" threw an unhandled exception.")]
private partial void ToolCallError(string toolName, Exception exception);
}

View file

@ -0,0 +1,161 @@
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol.Server;
/// <summary>
/// Provides filter collections for MCP server handlers.
/// </summary>
/// <remarks>
/// This class contains collections of filters that can be applied to various MCP server handlers.
/// This allows for middleware-style composition where filters can perform actions before and after the inner handler.
/// </remarks>
public sealed class McpServerFilters
{
/// <summary>
/// Gets the filters for the list tools handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that return a list of available tools when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ToolsList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more tools.
/// </para>
/// <para>
/// These filters work alongside any tools defined in the <see cref="McpServerTool"/> collection.
/// Tools from both sources will be combined when returning results to clients.
/// </para>
/// </remarks>
public List<McpRequestFilter<ListToolsRequestParams, ListToolsResult>> ListToolsFilters { get; } = new();
/// <summary>
/// Gets the filters for the call tool handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that are invoked when a client makes a call to a tool that isn't found in the <see cref="McpServerTool"/> collection.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ToolsCall"/> requests. The handler should implement logic to execute the requested tool and return appropriate results.
/// </remarks>
public List<McpRequestFilter<CallToolRequestParams, CallToolResult>> CallToolFilters { get; } = new();
/// <summary>
/// Gets the filters for the list prompts handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that return a list of available prompts when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.PromptsList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more prompts.
/// </para>
/// <para>
/// These filters work alongside any prompts defined in the <see cref="McpServerPrompt"/> collection.
/// Prompts from both sources will be combined when returning results to clients.
/// </para>
/// </remarks>
public List<McpRequestFilter<ListPromptsRequestParams, ListPromptsResult>> ListPromptsFilters { get; } = new();
/// <summary>
/// Gets the filters for the get prompt handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that are invoked when a client requests details for a specific prompt that isn't found in the <see cref="McpServerPrompt"/> collection.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.PromptsGet"/> requests. The handler should implement logic to fetch or generate the requested prompt and return appropriate results.
/// </remarks>
public List<McpRequestFilter<GetPromptRequestParams, GetPromptResult>> GetPromptFilters { get; } = new();
/// <summary>
/// Gets the filters for the list resource templates handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that return a list of available resource templates when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesTemplatesList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resource templates.
/// </remarks>
public List<McpRequestFilter<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>> ListResourceTemplatesFilters { get; } = new();
/// <summary>
/// Gets the filters for the list resources handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that return a list of available resources when requested by a client.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesList"/> requests. It supports pagination through the cursor mechanism,
/// where the client can make repeated calls with the cursor returned by the previous call to retrieve more resources.
/// </remarks>
public List<McpRequestFilter<ListResourcesRequestParams, ListResourcesResult>> ListResourcesFilters { get; } = new();
/// <summary>
/// Gets the filters for the read resource handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that are invoked when a client requests the content of a specific resource identified by its URI.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesRead"/> requests. The handler should implement logic to locate and retrieve the requested resource.
/// </remarks>
public List<McpRequestFilter<ReadResourceRequestParams, ReadResourceResult>> ReadResourceFilters { get; } = new();
/// <summary>
/// Gets the filters for the complete handler pipeline.
/// </summary>
/// <remarks>
/// These filters wrap handlers that provide auto-completion suggestions for prompt arguments or resource references in the Model Context Protocol.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.CompletionComplete"/> requests. The handler processes auto-completion requests, returning a list of suggestions based on the
/// reference type and current argument value.
/// </remarks>
public List<McpRequestFilter<CompleteRequestParams, CompleteResult>> CompleteFilters { get; } = new();
/// <summary>
/// Gets the filters for the subscribe to resources handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that are invoked when a client wants to receive notifications about changes to specific resources or resource patterns.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesSubscribe"/> requests. The handler should implement logic to register the client's interest in the specified resources
/// and set up the necessary infrastructure to send notifications when those resources change.
/// </para>
/// <para>
/// After a successful subscription, the server should send resource change notifications to the client
/// whenever a relevant resource is created, updated, or deleted.
/// </para>
/// </remarks>
public List<McpRequestFilter<SubscribeRequestParams, EmptyResult>> SubscribeToResourcesFilters { get; } = new();
/// <summary>
/// Gets the filters for the unsubscribe from resources handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that are invoked when a client wants to stop receiving notifications about previously subscribed resources.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.ResourcesUnsubscribe"/> requests. The handler should implement logic to remove the client's subscriptions to the specified resources
/// and clean up any associated resources.
/// </para>
/// <para>
/// After a successful unsubscription, the server should no longer send resource change notifications
/// to the client for the specified resources.
/// </para>
/// </remarks>
public List<McpRequestFilter<UnsubscribeRequestParams, EmptyResult>> UnsubscribeFromResourcesFilters { get; } = new();
/// <summary>
/// Gets the filters for the set logging level handler pipeline.
/// </summary>
/// <remarks>
/// <para>
/// These filters wrap handlers that process <see cref="RequestMethods.LoggingSetLevel"/> requests from clients. When set, it enables
/// clients to control which log messages they receive by specifying a minimum severity threshold.
/// The filters can modify, log, or perform additional operations on requests and responses for
/// <see cref="RequestMethods.LoggingSetLevel"/> requests.
/// </para>
/// <para>
/// After handling a level change request, the server typically begins sending log messages
/// at or above the specified level to the client as notifications/message notifications.
/// </para>
/// </remarks>
public List<McpRequestFilter<SetLevelRequestParams, EmptyResult>> SetLoggingLevelFilters { get; } = new();
}

View file

@ -79,4 +79,14 @@ public sealed class McpServerOptions
/// </para>
/// </remarks>
public Implementation? KnownClientInfo { get; set; }
/// <summary>
/// Gets the filter collections for MCP server handlers.
/// </summary>
/// <remarks>
/// This property provides access to filter collections that can be used to modify the behavior
/// of various MCP server handlers. Filters are applied in reverse order, so the last filter
/// added will be the outermost (first to execute).
/// </remarks>
public McpServerFilters Filters { get; } = new();
}

View file

@ -20,7 +20,7 @@ namespace ModelContextProtocol.Server;
/// </para>
/// <para>
/// Most commonly, <see cref="McpServerPrompt"/> instances are created using the static <see cref="M:McpServerPrompt.Create"/> methods.
/// These methods enable creating an <see cref="McpServerPrompt"/> for a method, specified via a <see cref="Delegate"/> or
/// These methods enable creating an <see cref="McpServerPrompt"/> for a method, specified via a <see cref="Delegate"/> or
/// <see cref="MethodInfo"/>, and are what are used implicitly by WithPromptsFromAssembly and WithPrompts. The <see cref="M:McpServerPrompt.Create"/> methods
/// create <see cref="McpServerPrompt"/> instances capable of working with a large variety of .NET method signatures, automatically handling
/// how parameters are marshaled into the method from the JSON received from the MCP client, and how the return value is marshaled back
@ -61,15 +61,15 @@ namespace ModelContextProtocol.Server;
/// </item>
/// <item>
/// <description>
/// When the <see cref="McpServerPrompt"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
/// When the <see cref="McpServerPrompt"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
/// <see cref="McpServerPromptCreateOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
/// according to <see cref="IServiceProviderIsService"/> will be resolved from the <see cref="IServiceProvider"/> provided to
/// according to <see cref="IServiceProviderIsService"/> will be resolved from the <see cref="IServiceProvider"/> provided to
/// <see cref="GetAsync"/> rather than from the argument collection.
/// </description>
/// </item>
/// <item>
/// <description>
/// Any parameter attributed with <see cref="FromKeyedServicesAttribute"/> will similarly be resolved from the
/// Any parameter attributed with <see cref="FromKeyedServicesAttribute"/> will similarly be resolved from the
/// <see cref="IServiceProvider"/> provided to <see cref="GetAsync"/> rather than from the argument collection.
/// </description>
/// </item>
@ -80,7 +80,7 @@ namespace ModelContextProtocol.Server;
/// </para>
/// <para>
/// In general, the data supplied via the <see cref="GetPromptRequestParams.Arguments"/>'s dictionary is passed along from the caller and
/// should thus be considered unvalidated and untrusted. To provide validated and trusted data to the invocation of the prompt, consider having
/// should thus be considered unvalidated and untrusted. To provide validated and trusted data to the invocation of the prompt, consider having
/// the prompt be an instance method, referring to data stored in the instance, or using an instance or parameters resolved from the <see cref="IServiceProvider"/>
/// to provide data to the method.
/// </para>
@ -128,6 +128,15 @@ public abstract class McpServerPrompt : IMcpServerPrimitive
/// </remarks>
public abstract Prompt ProtocolPrompt { get; }
/// <summary>
/// Gets the metadata for this prompt instance.
/// </summary>
/// <remarks>
/// Contains attributes from the associated MethodInfo and declaring class (if any),
/// with class-level attributes appearing before method-level attributes.
/// </remarks>
public abstract IReadOnlyList<object> Metadata { get; }
/// <summary>
/// Gets the prompt, rendering it with the provided request parameters and returning the prompt result.
/// </summary>
@ -170,7 +179,7 @@ public abstract class McpServerPrompt : IMcpServerPrimitive
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="method"/> is an instance method but <paramref name="target"/> is <see langword="null"/>.</exception>
public static McpServerPrompt Create(
MethodInfo method,
MethodInfo method,
object? target = null,
McpServerPromptCreateOptions? options = null) =>
AIFunctionMcpServerPrompt.Create(method, target, options);

View file

@ -68,6 +68,15 @@ public sealed class McpServerPromptCreateOptions
/// </remarks>
public AIJsonSchemaCreateOptions? SchemaCreateOptions { get; set; }
/// <summary>
/// Gets or sets the metadata associated with the prompt.
/// </summary>
/// <remarks>
/// Metadata includes information such as the attributes extracted from the method and its declaring class.
/// If not provided, metadata will be automatically generated for methods created via reflection.
/// </remarks>
public IReadOnlyList<object>? Metadata { get; set; }
/// <summary>
/// Creates a shallow clone of the current <see cref="McpServerPromptCreateOptions"/> instance.
/// </summary>
@ -80,5 +89,6 @@ public sealed class McpServerPromptCreateOptions
Description = Description,
SerializerOptions = SerializerOptions,
SchemaCreateOptions = SchemaCreateOptions,
Metadata = Metadata,
};
}

View file

@ -11,13 +11,13 @@ namespace ModelContextProtocol.Server;
/// <remarks>
/// <para>
/// <see cref="McpServerResource"/> is an abstract base class that represents an MCP resource for use in the server (as opposed
/// to <see cref="Resource"/> or <see cref="ResourceTemplate"/>, which provide the protocol representations of a resource). Instances of
/// to <see cref="Resource"/> or <see cref="ResourceTemplate"/>, which provide the protocol representations of a resource). Instances of
/// <see cref="McpServerResource"/> can be added into a <see cref="IServiceCollection"/> to be picked up automatically when
/// <see cref="McpServerFactory"/> is used to create an <see cref="IMcpServer"/>, or added into a <see cref="McpServerPrimitiveCollection{McpServerResource}"/>.
/// </para>
/// <para>
/// Most commonly, <see cref="McpServerResource"/> instances are created using the static <see cref="M:McpServerResource.Create"/> methods.
/// These methods enable creating an <see cref="McpServerResource"/> for a method, specified via a <see cref="Delegate"/> or
/// These methods enable creating an <see cref="McpServerResource"/> for a method, specified via a <see cref="Delegate"/> or
/// <see cref="MethodInfo"/>, and are what are used implicitly by WithResourcesFromAssembly and
/// <see cref="M:McpServerBuilderExtensions.WithResources"/>. The <see cref="M:McpServerResource.Create"/> methods
/// create <see cref="McpServerResource"/> instances capable of working with a large variety of .NET method signatures, automatically handling
@ -62,15 +62,15 @@ namespace ModelContextProtocol.Server;
/// </item>
/// <item>
/// <description>
/// When the <see cref="McpServerResource"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
/// When the <see cref="McpServerResource"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
/// <see cref="McpServerResourceCreateOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
/// according to <see cref="IServiceProviderIsService"/> will be resolved from the <see cref="IServiceProvider"/> provided to the
/// according to <see cref="IServiceProviderIsService"/> will be resolved from the <see cref="IServiceProvider"/> provided to the
/// resource invocation rather than from the argument collection.
/// </description>
/// </item>
/// <item>
/// <description>
/// Any parameter attributed with <see cref="FromKeyedServicesAttribute"/> will similarly be resolved from the
/// Any parameter attributed with <see cref="FromKeyedServicesAttribute"/> will similarly be resolved from the
/// <see cref="IServiceProvider"/> provided to the resource invocation rather than from the argument collection.
/// </description>
/// </item>
@ -149,6 +149,15 @@ public abstract class McpServerResource : IMcpServerPrimitive
/// </remarks>
public virtual Resource? ProtocolResource => ProtocolResourceTemplate.AsResource();
/// <summary>
/// Gets the metadata for this resource instance.
/// </summary>
/// <remarks>
/// Contains attributes from the associated MethodInfo and declaring class (if any),
/// with class-level attributes appearing before method-level attributes.
/// </remarks>
public abstract IReadOnlyList<object> Metadata { get; }
/// <summary>
/// Gets the resource, rendering it with the provided request parameters and returning the resource result.
/// </summary>
@ -192,7 +201,7 @@ public abstract class McpServerResource : IMcpServerPrimitive
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="method"/> is an instance method but <paramref name="target"/> is <see langword="null"/>.</exception>
public static McpServerResource Create(
MethodInfo method,
MethodInfo method,
object? target = null,
McpServerResourceCreateOptions? options = null) =>
AIFunctionMcpServerResource.Create(method, target, options);

View file

@ -83,6 +83,15 @@ public sealed class McpServerResourceCreateOptions
/// </remarks>
public AIJsonSchemaCreateOptions? SchemaCreateOptions { get; set; }
/// <summary>
/// Gets or sets the metadata associated with the resource.
/// </summary>
/// <remarks>
/// Metadata includes information such as attributes extracted from the method and its declaring class.
/// If not provided, metadata will be automatically generated for methods created via reflection.
/// </remarks>
public IReadOnlyList<object>? Metadata { get; set; }
/// <summary>
/// Creates a shallow clone of the current <see cref="McpServerResourceCreateOptions"/> instance.
/// </summary>
@ -97,5 +106,6 @@ public sealed class McpServerResourceCreateOptions
MimeType = MimeType,
SerializerOptions = SerializerOptions,
SchemaCreateOptions = SchemaCreateOptions,
Metadata = Metadata,
};
}

View file

@ -20,7 +20,7 @@ namespace ModelContextProtocol.Server;
/// </para>
/// <para>
/// Most commonly, <see cref="McpServerTool"/> instances are created using the static <see cref="M:McpServerTool.Create"/> methods.
/// These methods enable creating an <see cref="McpServerTool"/> for a method, specified via a <see cref="Delegate"/> or
/// These methods enable creating an <see cref="McpServerTool"/> for a method, specified via a <see cref="Delegate"/> or
/// <see cref="MethodInfo"/>, and are what are used implicitly by WithToolsFromAssembly and WithTools. The <see cref="M:McpServerTool.Create"/> methods
/// create <see cref="McpServerTool"/> instances capable of working with a large variety of .NET method signatures, automatically handling
/// how parameters are marshaled into the method from the JSON received from the MCP client, and how the return value is marshaled back
@ -56,22 +56,22 @@ namespace ModelContextProtocol.Server;
/// <description>
/// <see cref="IProgress{ProgressNotificationValue}"/> parameters accepting <see cref="ProgressNotificationValue"/> values
/// are not included in the JSON schema and are bound to an <see cref="IProgress{ProgressNotificationValue}"/> instance manufactured
/// to forward progress notifications from the tool to the client. If the client included a <see cref="ProgressToken"/> in their request,
/// to forward progress notifications from the tool to the client. If the client included a <see cref="ProgressToken"/> in their request,
/// progress reports issued to this instance will propagate to the client as <see cref="NotificationMethods.ProgressNotification"/> notifications with
/// that token. If the client did not include a <see cref="ProgressToken"/>, the instance will ignore any progress reports issued to it.
/// </description>
/// </item>
/// <item>
/// <description>
/// When the <see cref="McpServerTool"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
/// When the <see cref="McpServerTool"/> is constructed, it may be passed an <see cref="IServiceProvider"/> via
/// <see cref="McpServerToolCreateOptions.Services"/>. Any parameter that can be satisfied by that <see cref="IServiceProvider"/>
/// according to <see cref="IServiceProviderIsService"/> will not be included in the generated JSON schema and will be resolved
/// according to <see cref="IServiceProviderIsService"/> will not be included in the generated JSON schema and will be resolved
/// from the <see cref="IServiceProvider"/> provided to <see cref="InvokeAsync"/> rather than from the argument collection.
/// </description>
/// </item>
/// <item>
/// <description>
/// Any parameter attributed with <see cref="FromKeyedServicesAttribute"/> will similarly be resolved from the
/// Any parameter attributed with <see cref="FromKeyedServicesAttribute"/> will similarly be resolved from the
/// <see cref="IServiceProvider"/> provided to <see cref="InvokeAsync"/> rather than from the argument
/// collection, and will not be included in the generated JSON schema.
/// </description>
@ -79,13 +79,13 @@ namespace ModelContextProtocol.Server;
/// </list>
/// </para>
/// <para>
/// All other parameters are deserialized from the <see cref="JsonElement"/>s in the <see cref="CallToolRequestParams.Arguments"/> dictionary,
/// using the <see cref="JsonSerializerOptions"/> supplied in <see cref="McpServerToolCreateOptions.SerializerOptions"/>, or if none was provided,
/// All other parameters are deserialized from the <see cref="JsonElement"/>s in the <see cref="CallToolRequestParams.Arguments"/> dictionary,
/// using the <see cref="JsonSerializerOptions"/> supplied in <see cref="McpServerToolCreateOptions.SerializerOptions"/>, or if none was provided,
/// using <see cref="McpJsonUtilities.DefaultOptions"/>.
/// </para>
/// <para>
/// In general, the data supplied via the <see cref="CallToolRequestParams.Arguments"/>'s dictionary is passed along from the caller and
/// should thus be considered unvalidated and untrusted. To provide validated and trusted data to the invocation of the tool, consider having
/// should thus be considered unvalidated and untrusted. To provide validated and trusted data to the invocation of the tool, consider having
/// the tool be an instance method, referring to data stored in the instance, or using an instance or parameters resolved from the <see cref="IServiceProvider"/>
/// to provide data to the method.
/// </para>
@ -141,6 +141,15 @@ public abstract class McpServerTool : IMcpServerPrimitive
/// <summary>Gets the protocol <see cref="Tool"/> type for this instance.</summary>
public abstract Tool ProtocolTool { get; }
/// <summary>
/// Gets the metadata for this tool instance.
/// </summary>
/// <remarks>
/// Contains attributes from the associated MethodInfo and declaring class (if any),
/// with class-level attributes appearing before method-level attributes.
/// </remarks>
public abstract IReadOnlyList<object> Metadata { get; }
/// <summary>Invokes the <see cref="McpServerTool"/>.</summary>
/// <param name="request">The request information resulting in the invocation of this tool.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
@ -172,7 +181,7 @@ public abstract class McpServerTool : IMcpServerPrimitive
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="method"/> is an instance method but <paramref name="target"/> is <see langword="null"/>.</exception>
public static McpServerTool Create(
MethodInfo method,
MethodInfo method,
object? target = null,
McpServerToolCreateOptions? options = null) =>
AIFunctionMcpServerTool.Create(method, target, options);

View file

@ -80,7 +80,7 @@ public sealed class McpServerToolCreateOptions
public bool? Destructive { get; set; }
/// <summary>
/// Gets or sets whether calling the tool repeatedly with the same arguments
/// Gets or sets whether calling the tool repeatedly with the same arguments
/// will have no additional effect on its environment.
/// </summary>
/// <remarks>
@ -155,6 +155,15 @@ public sealed class McpServerToolCreateOptions
/// </remarks>
public AIJsonSchemaCreateOptions? SchemaCreateOptions { get; set; }
/// <summary>
/// Gets or sets the metadata associated with the tool.
/// </summary>
/// <remarks>
/// Metadata includes information such as attributes extracted from the method and its declaring class.
/// If not provided, metadata will be automatically generated for methods created via reflection.
/// </remarks>
public IReadOnlyList<object>? Metadata { get; set; }
/// <summary>
/// Creates a shallow clone of the current <see cref="McpServerToolCreateOptions"/> instance.
/// </summary>
@ -172,5 +181,6 @@ public sealed class McpServerToolCreateOptions
UseStructuredContent = UseStructuredContent,
SerializerOptions = SerializerOptions,
SchemaCreateOptions = SchemaCreateOptions,
Metadata = Metadata,
};
}

View file

@ -1,3 +1,6 @@
using System.Security.Claims;
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol.Server;
/// <summary>
@ -14,20 +17,26 @@ public sealed class RequestContext<TParams>
/// <summary>The server with which this instance is associated.</summary>
private IMcpServer _server;
private IDictionary<string, object?>? _items;
/// <summary>
/// Initializes a new instance of the <see cref="RequestContext{TParams}"/> class with the specified server.
/// Initializes a new instance of the <see cref="RequestContext{TParams}"/> class with the specified server and JSON-RPC request.
/// </summary>
/// <param name="server">The server with which this instance is associated.</param>
public RequestContext(IMcpServer server)
/// <param name="jsonRpcRequest">The JSON-RPC request associated with this context.</param>
public RequestContext(IMcpServer server, JsonRpcRequest jsonRpcRequest)
{
Throw.IfNull(server);
Throw.IfNull(jsonRpcRequest);
_server = server;
JsonRpcRequest = jsonRpcRequest;
Services = server.Services;
User = jsonRpcRequest.Context?.User;
}
/// <summary>Gets or sets the server with which this instance is associated.</summary>
public IMcpServer Server
public IMcpServer Server
{
get => _server;
set
@ -37,6 +46,21 @@ public sealed class RequestContext<TParams>
}
}
/// <summary>
/// Gets or sets a key/value collection that can be used to share data within the scope of this request.
/// </summary>
public IDictionary<string, object?> Items
{
get
{
return _items ??= new Dictionary<string, object?>();
}
set
{
_items = value;
}
}
/// <summary>Gets or sets the services associated with this request.</summary>
/// <remarks>
/// This may not be the same instance stored in <see cref="IMcpServer.Services"/>
@ -46,6 +70,23 @@ public sealed class RequestContext<TParams>
/// </remarks>
public IServiceProvider? Services { get; set; }
/// <summary>Gets or sets the user associated with this request.</summary>
public ClaimsPrincipal? User { get; set; }
/// <summary>Gets or sets the parameters associated with this request.</summary>
public TParams? Params { get; set; }
/// <summary>
/// Gets or sets the primitive that matched the request.
/// </summary>
public IMcpServerPrimitive? MatchedPrimitive { get; set; }
/// <summary>
/// Gets the JSON-RPC request associated with this context.
/// </summary>
/// <remarks>
/// This property provides access to the complete JSON-RPC request that initiated this handler invocation,
/// including the method name, parameters, request ID, and associated transport and user information.
/// </remarks>
public JsonRpcRequest JsonRpcRequest { get; }
}

View file

@ -0,0 +1,61 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol;
using System.Security.Claims;
namespace ModelContextProtocol.Server;
/// <summary>Augments a service provider with additional request-related services.</summary>
internal sealed class RequestServiceProvider<TRequestParams>(RequestContext<TRequestParams> request) :
IServiceProvider, IKeyedServiceProvider, IServiceProviderIsService, IServiceProviderIsKeyedService,
IDisposable, IAsyncDisposable
where TRequestParams : RequestParams
{
private readonly IServiceProvider? _innerServices = request.Services;
/// <summary>Gets the request associated with this instance.</summary>
public RequestContext<TRequestParams> Request => request;
/// <summary>Gets whether the specified type is in the list of additional types this service provider wraps around the one in a provided request's services.</summary>
public static bool IsAugmentedWith(Type serviceType) =>
serviceType == typeof(RequestContext<TRequestParams>) ||
serviceType == typeof(IMcpServer) ||
serviceType == typeof(IProgress<ProgressNotificationValue>) ||
serviceType == typeof(ClaimsPrincipal);
/// <inheritdoc />
public object? GetService(Type serviceType) =>
serviceType == typeof(RequestContext<TRequestParams>) ? request :
serviceType == typeof(IMcpServer) ? request.Server :
serviceType == typeof(IProgress<ProgressNotificationValue>) ?
(request.Params?.ProgressToken is { } progressToken ? new TokenProgress(request.Server, progressToken) : NullProgress.Instance) :
serviceType == typeof(ClaimsPrincipal) ? request.User :
_innerServices?.GetService(serviceType);
/// <inheritdoc />
public bool IsService(Type serviceType) =>
IsAugmentedWith(serviceType) ||
(_innerServices as IServiceProviderIsService)?.IsService(serviceType) is true;
/// <inheritdoc />
public bool IsKeyedService(Type serviceType, object? serviceKey) =>
(serviceKey is null && IsService(serviceType)) ||
(_innerServices as IServiceProviderIsKeyedService)?.IsKeyedService(serviceType, serviceKey) is true;
/// <inheritdoc />
public object? GetKeyedService(Type serviceType, object? serviceKey) =>
serviceKey is null ? GetService(serviceType) :
(_innerServices as IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey);
/// <inheritdoc />
public object GetRequiredKeyedService(Type serviceType, object? serviceKey) =>
GetKeyedService(serviceType, serviceKey) ??
throw new InvalidOperationException($"No service of type '{serviceType}' with key '{serviceKey}' is registered.");
/// <inheritdoc />
public void Dispose() =>
(_innerServices as IDisposable)?.Dispose();
/// <inheritdoc />
public ValueTask DisposeAsync() =>
_innerServices is IAsyncDisposable asyncDisposable ? asyncDisposable.DisposeAsync() : default;
}

View file

@ -1,4 +1,5 @@
using ModelContextProtocol.Protocol;
using System.Security.Claims;
using System.Threading.Channels;
namespace ModelContextProtocol.Server;
@ -9,7 +10,7 @@ namespace ModelContextProtocol.Server;
/// <remarks>
/// <para>
/// This transport provides one-way communication from server to client using the SSE protocol over HTTP,
/// while receiving client messages through a separate mechanism. It writes messages as
/// while receiving client messages through a separate mechanism. It writes messages as
/// SSE events to a response stream, typically associated with an HTTP response.
/// </para>
/// <para>
@ -41,7 +42,7 @@ public sealed class SseResponseStreamTransport(Stream sseResponseStream, string?
/// </summary>
/// <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>
public async Task RunAsync(CancellationToken cancellationToken)
public async Task RunAsync(CancellationToken cancellationToken = default)
{
_isConnected = true;
await _sseWriter.WriteAllAsync(sseResponseStream, cancellationToken).ConfigureAwait(false);
@ -64,6 +65,7 @@ public sealed class SseResponseStreamTransport(Stream sseResponseStream, string?
/// <inheritdoc/>
public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
Throw.IfNull(message);
await _sseWriter.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
}
@ -76,8 +78,8 @@ public sealed class SseResponseStreamTransport(Stream sseResponseStream, string?
/// <exception cref="InvalidOperationException">Thrown when there is an attempt to process a message before calling <see cref="RunAsync(CancellationToken)"/>.</exception>
/// <remarks>
/// <para>
/// This method is the entry point for processing client-to-server communication in the SSE transport model.
/// While the SSE protocol itself is unidirectional (server to client), this method allows bidirectional
/// This method is the entry point for processing client-to-server communication in the SSE transport model.
/// While the SSE protocol itself is unidirectional (server to client), this method allows bidirectional
/// communication by handling HTTP POST requests sent to the message endpoint.
/// </para>
/// <para>
@ -85,11 +87,11 @@ public sealed class SseResponseStreamTransport(Stream sseResponseStream, string?
/// process the message and make it available to the MCP server via the <see cref="MessageReader"/> channel.
/// </para>
/// <para>
/// This method validates that the transport is connected before processing the message, ensuring proper
/// sequencing of operations in the transport lifecycle.
/// 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.
/// </para>
/// </remarks>
public async Task OnMessageReceivedAsync(JsonRpcMessage message, CancellationToken cancellationToken)
public async Task OnMessageReceivedAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
Throw.IfNull(message);

View file

@ -26,6 +26,8 @@ internal sealed class SseWriter(string? messageEndpoint = null, BoundedChannelOp
public Task WriteAllAsync(Stream sseResponseStream, CancellationToken cancellationToken)
{
Throw.IfNull(sseResponseStream);
// When messageEndpoint is set, the very first SSE event isn't really an IJsonRpcMessage, but there's no API to write a single
// item of a different type, so we fib and special-case the "endpoint" event type in the formatter.
if (messageEndpoint is not null && !_messages.Writer.TryWrite(new SseItem<JsonRpcMessage?>(null, "endpoint")))

Some files were not shown because too many files have changed in this diff Show more