Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/concepts/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ The following request filter methods are available on `IMcpRequestFilterBuilder`
- `AddUnsubscribeFromResourcesFilter` - Filter for resource unsubscription handlers
- `AddSetLoggingLevelFilter` - Filter for logging level handlers

The Tasks extension and ASP.NET Core tool authorization use alternate-result `tools/call` filters.
Alternate-result filters run in registration order outside the ordinary `AddCallToolFilter`
pipeline. The Tasks filter creates a task at its position in that order and runs its remaining
pipeline in the background. Consequently, alternate-result filters registered before Tasks run
before task creation, while those registered after Tasks run in the background before the
ordinary filters and tool. ASP.NET Core authorization is registered before Tasks, so an
unauthorized call is rejected without creating a task.

Ordinary call-tool filters run exactly once after all alternate-result filters and before the tool.
For task-backed calls, ordinary filters execute in the background after task creation. An explicit
`CallToolWithAlternateHandler` remains a full replacement and cannot be combined with ordinary
call-tool filters.

## Message Filters

In addition to the request-specific filters above, there are low-level message filters that intercept all JSON-RPC messages before they are routed to specific handlers.
Expand Down
11 changes: 11 additions & 0 deletions docs/concepts/tasks/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ When tasks are enabled with `WithTasks` the SDK automatically:
- Plumbs a `CancellationToken` through to the tool that fires when the client invokes
`tasks/cancel`, so cancellation propagates cooperatively.

Alternate-result `tools/call` filters run in registration order, with the Tasks filter creating a
task at its position in that order. Filters before Tasks run before task creation. Filters after
Tasks run in the background before the ordinary filter pipeline. ASP.NET Core tool authorization
uses an alternate-result filter registered before Tasks, so an unauthorized call does not create
a task.

Ordinary `tools/call` filters still run exactly once for task-backed calls. They execute in the
background after the task record is created and before the tool body, so validation and telemetry
continue to apply. Each background invocation gets an independent DI scope that remains alive
until the tool pipeline completes.

For production scenarios that need durability, session isolation, multi-process routing, or
TTL-based cleanup, implement <xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore> yourself
(see [Implementing a custom task store](#implementing-a-custom-task-store) below).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ private static void CheckListToolsFilter(McpServerOptions options)

private void ConfigureCallToolFilter(McpServerOptions options)
{
options.Filters.Request.CallToolFilters.Add(next => async (context, cancellationToken) =>
#pragma warning disable MCPEXP002 // Authorization must run in the alternate-result pipeline before task dispatch.
Comment thread
PranavSenthilnathan marked this conversation as resolved.
options.Filters.Request.CallToolWithAlternateFilters.Add(next => async (context, cancellationToken) =>
{
var authResult = await GetAuthorizationResultAsync(context.User, context.MatchedPrimitive, context.Services, context);
if (!authResult.Succeeded)
Expand All @@ -93,11 +94,13 @@ private void ConfigureCallToolFilter(McpServerOptions options)

return await next(context, cancellationToken);
});
#pragma warning restore MCPEXP002
}

private static void CheckCallToolFilter(McpServerOptions options)
{
options.Filters.Request.CallToolFilters.Add(next => async (context, cancellationToken) =>
#pragma warning disable MCPEXP002 // Keep the authorization guard in the same pipeline as authorization.
options.Filters.Request.CallToolWithAlternateFilters.Add(next => async (context, cancellationToken) =>
{
if (HasAuthorizationMetadata(context.MatchedPrimitive)
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
Expand All @@ -107,6 +110,7 @@ private static void CheckCallToolFilter(McpServerOptions options)

return await next(context, cancellationToken);
});
#pragma warning restore MCPEXP002
}

private void ConfigureListResourcesFilter(McpServerOptions options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ public static IMcpServerBuilder WithHttpTransport(this IMcpServerBuilder builder
/// <remarks>
/// This method automatically configures authorization filters for all MCP server handlers. These filters respect
/// authorization attributes such as <see cref="AuthorizeAttribute"/>
/// and <see cref="AllowAnonymousAttribute"/>.
/// and <see cref="AllowAnonymousAttribute"/>. Tool authorization runs in the alternate-result pipeline before
/// the Tasks extension dispatches background execution, so an unauthorized tool call does not create a task.
/// </remarks>
public static IMcpServerBuilder AddAuthorizationFilters(this IMcpServerBuilder builder)
{
Expand Down
21 changes: 13 additions & 8 deletions src/ModelContextProtocol.Core/Server/McpRequestFilters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,14 @@ public IList<McpRequestFilter<ListToolsRequestParams, ListToolsResult>> ListTool
/// <see cref="RequestMethods.ToolsCall"/> requests. The handler should implement logic to execute the requested tool and return appropriate results.
/// </para>
/// <para>
/// Cannot be used together with <see cref="CallToolWithAlternateFilters"/>. If both are non-empty at configuration time,
/// an <see cref="InvalidOperationException"/> will be thrown. This can happen indirectly when combining features that
/// register different tool-call filter styles, such as authorization filters (which use this collection) and the
/// tasks extension (which uses <see cref="CallToolWithAlternateFilters"/>).
/// These filters run inside <see cref="CallToolWithAlternateFilters"/>. Each ordinary filter runs exactly once
/// when an alternate-result filter invokes the ordinary tool pipeline. For task-backed calls, that invocation
/// occurs in the background after the task record is created and before the matched tool is executed. Filters
/// that must run before task creation should use the alternate-result pipeline instead.
/// </para>
/// <para>
/// These filters cannot be used with an explicit <see cref="McpServerHandlers.CallToolWithAlternateHandler"/>,
/// which replaces the ordinary tool-call pipeline rather than augmenting it.
/// </para>
/// </remarks>
public IList<McpRequestFilter<CallToolRequestParams, CallToolResult>> CallToolFilters
Expand All @@ -71,10 +75,11 @@ public IList<McpRequestFilter<CallToolRequestParams, CallToolResult>> CallToolFi
/// subtype for asynchronous execution.
/// </para>
/// <para>
/// Cannot be used together with <see cref="CallToolFilters"/>. If both are non-empty at configuration time,
/// an <see cref="InvalidOperationException"/> will be thrown. This can happen indirectly when combining features that
/// register different tool-call filter styles, such as the tasks extension (which uses this collection) and
/// authorization filters (which use <see cref="CallToolFilters"/>).
/// When no explicit <see cref="McpServerHandlers.CallToolWithAlternateHandler"/> is configured, these filters
/// compose outside <see cref="CallToolFilters"/>. Primitive matching occurs before either filter family runs, then
/// the ordinary pipeline is adapted to <see cref="ResultOrAlternate{TResult}"/> before these filters are applied.
/// Alternate-result filters run in registration order. If one filter dispatches the remainder of the pipeline
/// asynchronously, filters registered after it execute as part of that asynchronous operation.
/// </para>
/// </remarks>
[Experimental(Experimentals.Subclassing_DiagnosticId, UrlFormat = Experimentals.Subclassing_Url)]
Expand Down
4 changes: 3 additions & 1 deletion src/ModelContextProtocol.Core/Server/McpServerHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ public McpRequestHandler<CallToolRequestParams, CallToolResult>? CallToolHandler
/// a <see cref="CallToolResult"/> for immediate results or an alternate <see cref="Result"/> subtype.
/// </para>
/// <para>
/// Cannot be set if <see cref="CallToolHandler"/> is already set.
/// This is a low-level full replacement for the ordinary tool-call pipeline. It cannot be set if
/// <see cref="CallToolHandler"/> is already set, and it cannot be composed with ordinary
/// <see cref="McpRequestFilters.CallToolFilters"/>.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException"><see cref="CallToolHandler"/> is already set.</exception>
Expand Down
Loading
Loading