Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions ModelContextProtocol.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<Project Path="src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj" />
<Project Path="src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj" />
<Project Path="src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj" />
<Project Path="src/ModelContextProtocol.Extensions.Tasks/ModelContextProtocol.Extensions.Tasks.csproj" />
<Project Path="src/ModelContextProtocol/ModelContextProtocol.csproj" />
</Folder>
<Folder Name="/tests/">
Expand Down
8 changes: 4 additions & 4 deletions docs/concepts/stateless/stateless.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ In stateless mode, each HTTP request creates and disposes a short-lived `McpServ

## Tasks and session modes

[Tasks](xref:tasks) enable a "call-now, fetch-later" pattern for long-running tool calls. Task support depends on having an <xref:ModelContextProtocol.Server.IMcpTaskStore> configured (`McpServerOptions.TaskStore`), and behavior differs between session modes.
[Tasks](xref:tasks) enable a "call-now, fetch-later" pattern for long-running tool calls. Task support depends on having an <xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore> configured (enabled via `WithTasks`), and behavior differs between session modes.

### Stateless mode

Expand All @@ -600,9 +600,9 @@ In stateless mode, there is no `SessionId`, so the task store does not apply ses

### Stateful mode

In stateful mode, the `IMcpTaskStore` receives the session's `SessionId` on every operation — `CreateTaskAsync`, `GetTaskAsync`, `ListTasksAsync`, `CancelTaskAsync`, etc. The built-in <xref:ModelContextProtocol.Server.InMemoryMcpTaskStore> enforces session isolation: tasks created in one session cannot be accessed from another.
In stateful mode, the `IMcpTaskStore` receives the session's `SessionId` on every operation: `CreateTaskAsync`, `GetTaskAsync`, `ListTasksAsync`, `CancelTaskAsync`, etc. The built-in <xref:ModelContextProtocol.Extensions.Tasks.InMemoryMcpTaskStore> enforces session isolation: tasks created in one session cannot be accessed from another.

Tasks can outlive individual HTTP requests because the tool executes in the background after returning the initial `CreateTaskResult`. Task cleanup is governed by the task's TTL (time-to-live), not by session termination. However, the `InMemoryMcpTaskStore` loses all tasks if the server process restarts. For durable tasks, implement a custom <xref:ModelContextProtocol.Server.IMcpTaskStore> backed by an external store. See [Implementing a custom task store](xref:tasks#implementing-a-custom-task-store) for guidance.
Tasks can outlive individual HTTP requests because the tool executes in the background after returning the initial `CreateTaskResult`. Task cleanup is governed by the task's TTL (time-to-live), not by session termination. However, the `InMemoryMcpTaskStore` loses all tasks if the server process restarts. For durable tasks, implement a custom <xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore> backed by an external store. See [Implementing a custom task store](xref:tasks#implementing-a-custom-task-store) for guidance.

### Task cancellation vs request cancellation

Expand Down Expand Up @@ -654,7 +654,7 @@ The `EventStreamStore` itself has TTL-based limits (default: 2-hour event expira

### With tasks (experimental)

[Tasks](xref:tasks) are an experimental feature that enables a "call-now, fetch-later" pattern for long-running tool calls. When a client sends a task-augmented `tools/call` request, the server creates a task record in the <xref:ModelContextProtocol.Server.IMcpTaskStore>, starts the tool handler as a fire-and-forget background task, and returns the task ID immediately — the POST response completes **before the handler starts its real work**.
[Tasks](xref:tasks) are an experimental feature that enables a "call-now, fetch-later" pattern for long-running tool calls. When a client sends a task-augmented `tools/call` request, the server creates a task record in the <xref:ModelContextProtocol.Extensions.Tasks.IMcpTaskStore>, starts the tool handler as a fire-and-forget background task, and returns the task ID immediately, so the POST response completes **before the handler starts its real work**.

This means:

Expand Down
156 changes: 75 additions & 81 deletions docs/concepts/tasks/tasks.md

Large diffs are not rendered by default.

49 changes: 21 additions & 28 deletions samples/TasksExtension/Program.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
// Demonstrates the MCP tasks extension (SEP-2663):
//
// - A server is configured with InMemoryMcpTaskStore so that any [McpServerTool] invocation
// - A server is configured with .WithTasks(store) so that any [McpServerTool] invocation
// becomes a background task when the client opts in via the per-request _meta marker.
// - The client invokes the same tool two ways:
// 1. CallToolAsync — the SDK auto-polls until the task completes and returns the final
// CallToolResult, just like a synchronous call.
// 2. CallToolRawAsync — the caller drives the lifecycle manually (GetTaskAsync polls,
// CancelTaskAsync, etc.). Use this when you need to surface progress to a UI or stream
// status updates rather than block on a single await.
// - The client invokes the tool and manually drives the lifecycle via GetTaskAsync.
//
// Both server and client are wired together in-process over an in-memory pipe so the sample
// is self-contained — no separate server process or HTTP transport required.

#pragma warning disable MCPEXP001, MCPEXP002, MCPEXP004

using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Extensions.Tasks;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.ComponentModel;
Expand All @@ -21,28 +20,25 @@

Pipe clientToServerPipe = new(), serverToClientPipe = new();

await using McpServer server = McpServer.Create(
new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()),
new McpServerOptions
{
// Setting TaskStore is all that's needed for [McpServerTool]-attributed tools to be
// automatically wrapped as background tasks when the client opts in.
TaskStore = new InMemoryMcpTaskStore { DefaultPollIntervalMs = 250 },
ToolCollection = [McpServerTool.Create(SlowTools.RunReport, new() { Name = "run-report" })],
});
var store = new InMemoryMcpTaskStore { DefaultPollIntervalMs = 250 };

var services = new ServiceCollection();
services.AddMcpServer()
.WithTools([McpServerTool.Create(SlowTools.RunReport, new() { Name = "run-report" })])
.WithTasks(store);
services.AddSingleton<ITransport>(new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()));

await using var serviceProvider = services.BuildServiceProvider();
var server = serviceProvider.GetRequiredService<McpServer>();
_ = server.RunAsync();

await using McpClient client = await McpClient.CreateAsync(
new StreamClientTransport(clientToServerPipe.Writer.AsStream(), serverToClientPipe.Reader.AsStream()));

Console.WriteLine("=== CallToolAsync (auto-poll) ===");
var auto = await client.CallToolAsync(
new CallToolRequestParams { Name = "run-report" });
Console.WriteLine($" result: {((TextContentBlock)auto.Content[0]).Text}");
Console.WriteLine();
new StreamClientTransport(
serverInput: clientToServerPipe.Writer.AsStream(),
serverOutput: serverToClientPipe.Reader.AsStream()));

Console.WriteLine("=== CallToolRawAsync (manual poll) ===");
var raw = await client.CallToolRawAsync(new CallToolRequestParams { Name = "run-report" });
Console.WriteLine("=== CallToolAsTaskAsync (manual poll) ===");
var raw = await client.CallToolAsTaskAsync(new CallToolRequestParams { Name = "run-report" });
if (!raw.IsTask)
{
// Either the server doesn't advertise the tasks extension or it chose to run the call
Expand Down Expand Up @@ -88,9 +84,6 @@
continue;

case InputRequiredTaskResult inputRequired:
// The auto-poll path (CallToolAsync above) routes these through the registered
// ElicitationHandler/SamplingHandler automatically. The manual path needs to call
// UpdateTaskAsync with responses for each outstanding key.
Console.WriteLine($" poll {pollCount}: input requested ({inputRequired.InputRequests?.Count ?? 0} key(s))");
continue;
}
Expand Down
2 changes: 2 additions & 0 deletions samples/TasksExtension/TasksExtension.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

<ItemGroup>
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
<ProjectReference Include="..\..\src\ModelContextProtocol.Extensions.Tasks\ModelContextProtocol.Extensions.Tasks.csproj" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>

</Project>
Loading