Enable complete Tasks extension conformance coverage (#1741)

Copilot-Session: 7c99928c-a943-49f4-9a63-8b04a0e4d18b
This commit is contained in:
Pranav Senthilnathan 2026-07-27 12:45:25 -07:00 • committed by GitHub
commit 7838900e8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 190 additions and 14 deletions

View file

@ -0,0 +1,16 @@
namespace ModelContextProtocol.Extensions.Tasks;
/// <summary>
/// Specifies how a tool call participates in the MCP Tasks extension.
/// </summary>
public enum McpTaskExecutionMode
{
/// <summary>The tool always executes synchronously.</summary>
Synchronous,
/// <summary>The tool executes as a task when the client declares the Tasks extension.</summary>
Optional,
/// <summary>The tool requires the client to declare the Tasks extension.</summary>
Required,
}

View file

@ -29,13 +29,36 @@ public static class McpTasksBuilderExtensions
/// <param name="store">The task store.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpServerBuilder WithTasks(this IMcpServerBuilder builder, IMcpTaskStore store)
=> WithTasks(builder, store, static _ => { });
/// <summary>
/// Enables MCP Tasks support backed by the specified task store.
/// </summary>
/// <param name="builder">The server builder.</param>
/// <param name="store">The task store.</param>
/// <param name="configure">A callback that configures per-call task execution behavior.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
public static IMcpServerBuilder WithTasks(
this IMcpServerBuilder builder,
IMcpTaskStore store,
Action<McpTasksOptions> configure)
{
#if NET
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(store);
ArgumentNullException.ThrowIfNull(configure);
#else
if (builder is null) throw new ArgumentNullException(nameof(builder));
if (store is null) throw new ArgumentNullException(nameof(store));
if (configure is null) throw new ArgumentNullException(nameof(configure));
#endif
McpTasksOptions taskOptions = new();
configure(taskOptions);
#if NET
ArgumentNullException.ThrowIfNull(taskOptions.ExecutionModeSelector);
#else
if (taskOptions.ExecutionModeSelector is null) throw new ArgumentNullException(nameof(taskOptions.ExecutionModeSelector));
#endif
// Resolve ILoggerFactory from the provider (rather than requiring the caller to pass one) so the
@ -45,18 +68,21 @@ public static class McpTasksBuilderExtensions
sp => new McpTasksConfigureOptions(
store,
sp.GetRequiredService<IServiceScopeFactory>(),
sp.GetService<ILoggerFactory>()));
sp.GetService<ILoggerFactory>(),
taskOptions));
return builder;
}
private sealed class McpTasksConfigureOptions(
IMcpTaskStore store,
IServiceScopeFactory serviceScopeFactory,
ILoggerFactory? loggerFactory) : IConfigureOptions<McpServerOptions>
ILoggerFactory? loggerFactory,
McpTasksOptions taskOptions) : IConfigureOptions<McpServerOptions>
{
private readonly IMcpTaskStore _store = store;
private readonly IServiceScopeFactory _serviceScopeFactory = serviceScopeFactory;
private readonly ILogger _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<McpTasksConfigureOptions>();
private readonly McpTasksOptions _taskOptions = taskOptions;
private readonly ConcurrentDictionary<string, CancellationTokenSource> _cancellationSources = new(StringComparer.Ordinal);
public void Configure(McpServerOptions options)
@ -108,9 +134,28 @@ public static class McpTasksBuilderExtensions
options.Filters.Request.CallToolWithAlternateFilters.Count,
async (request, next, cancellationToken) =>
{
var executionMode = _taskOptions.ExecutionModeSelector(request);
if (executionMode is not McpTaskExecutionMode.Synchronous and
not McpTaskExecutionMode.Optional and
not McpTaskExecutionMode.Required)
{
throw new InvalidOperationException(
$"{nameof(McpTasksOptions.ExecutionModeSelector)} returned an invalid {nameof(McpTaskExecutionMode)} value.");
}
if (executionMode == McpTaskExecutionMode.Synchronous)
{
return await next(request, cancellationToken).ConfigureAwait(false);
}
if (!IsJuly2026OrLaterProtocolRequest(request.JsonRpcRequest) ||
!HasTaskExtensionOptIn(request.JsonRpcRequest))
{
if (executionMode == McpTaskExecutionMode.Required)
{
throw CreateMissingTasksCapabilityException();
}
return await next(request, cancellationToken).ConfigureAwait(false);
}

View file

@ -0,0 +1,22 @@
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
namespace ModelContextProtocol.Extensions.Tasks;
/// <summary>
/// Configures server-side MCP Tasks behavior.
/// </summary>
public sealed class McpTasksOptions
{
/// <summary>
/// Gets or sets the callback that selects task execution behavior for each tool call.
/// </summary>
/// <remarks>
/// The default treats every tool as task-capable, preserving the behavior of
/// <c>WithTasks</c>.
/// The callback may inspect <see cref="RequestContext{TParams}.MatchedPrimitive"/> to select
/// behavior from tool metadata without requiring Core to reference the Tasks extension.
/// </remarks>
public Func<RequestContext<CallToolRequestParams>, McpTaskExecutionMode> ExecutionModeSelector { get; set; } =
static _ => McpTaskExecutionMode.Optional;
}