Fix HTTP task filter composition (#1722)

This commit is contained in:
Pranav Senthilnathan 2026-07-24 13:33:19 -07:00 • committed by GitHub
commit a5179df39b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1164 additions and 286 deletions

View file

@ -32,6 +32,22 @@ The following request filter methods are available on `IMcpRequestFilterBuilder`
| `AddUnsubscribeFromResourcesFilter` | Resource unsubscription handlers |
| `AddSetLoggingLevelFilter` | 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.
Configure `WithTasks` before adding ordinary call-tool filters. Tasks validates this ordering when
its filter is installed because it must execute outside the ordinary call-tool pipeline.
## 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.

View file

@ -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 the [Implementing a custom task store](#implementing-a-custom-task-store) section).
@ -126,28 +137,6 @@ options.Handlers.CallToolWithAlternateHandler = async (context, ct) =>
> are mutually exclusive. Setting one while the other is already non-null throws
> `InvalidOperationException` at the property setter.
#### Task scope for server-initiated requests
When you start background work from a custom <xref:ModelContextProtocol.Server.McpServerHandlers.CallToolWithAlternateHandler?displayProperty=nameWithType>
(rather than the SDK's auto-wrapping), use <xref:ModelContextProtocol.Extensions.Tasks.McpTasksServerExtensions.CreateMcpTaskScope*>
to route elicitation, sampling, and `roots/list` calls through the task store as input requests
instead of direct JSON-RPC messages:
```csharp
using ModelContextProtocol.Extensions.Tasks;
using (server.CreateMcpTaskScope(taskId, taskStore))
{
// ElicitAsync/SampleAsync/RequestRootsAsync calls in here are surfaced as
// entries in the task's inputRequests, then await client responses via tasks/update.
var elicit = await server.ElicitAsync(elicitParams, ct);
}
```
`CreateMcpTaskScope` returns an `IDisposable` that restores the prior ambient context on
`Dispose`. The scope is established automatically for `[McpServerTool]` methods that run via
`WithTasks`, so this API is only needed for custom handlers.
### Client usage
#### Automatic polling