Add middleware and authz support for server-side handlers (#733)
This commit is contained in:
parent
0f0aad020b
commit
d344c65120
22 changed files with 2871 additions and 435 deletions
|
|
@ -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);
|
||||
13
src/ModelContextProtocol.Core/Server/McpRequestHandler.cs
Normal file
13
src/ModelContextProtocol.Core/Server/McpRequestHandler.cs
Normal 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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
161
src/ModelContextProtocol.Core/Server/McpServerFilters.cs
Normal file
161
src/ModelContextProtocol.Core/Server/McpServerFilters.cs
Normal 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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue