Skip to content

Commit 2ca96f5

Browse files
jeffhandleyCopilotTarek Mahmoud Sayed
authored
Extract Tasks into the ModelContextProtocol.Extensions.Tasks extension package (#1693)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
1 parent ffc557f commit 2ca96f5

63 files changed

Lines changed: 2559 additions & 1895 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ModelContextProtocol.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
<Project Path="src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj" />
7070
<Project Path="src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj" />
7171
<Project Path="src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj" />
72+
<Project Path="src/ModelContextProtocol.Extensions.Tasks/ModelContextProtocol.Extensions.Tasks.csproj" />
7273
<Project Path="src/ModelContextProtocol/ModelContextProtocol.csproj" />
7374
</Folder>
7475
<Folder Name="/tests/">

docs/concepts/stateless/stateless.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ In stateless mode, each HTTP request creates and disposes a short-lived `McpServ
590590

591591
## Tasks and session modes
592592

593-
[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.
593+
[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.
594594

595595
### Stateless mode
596596

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

601601
### Stateful mode
602602

603-
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.
603+
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.
604604

605-
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.
605+
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.
606606

607607
### Task cancellation vs request cancellation
608608

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

655655
### With tasks (experimental)
656656

657-
[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**.
657+
[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**.
658658

659659
This means:
660660

docs/concepts/tasks/tasks.md

Lines changed: 75 additions & 81 deletions
Large diffs are not rendered by default.

samples/TasksExtension/Program.cs

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

10+
#pragma warning disable MCPEXP001, MCPEXP002, MCPEXP004
11+
12+
using Microsoft.Extensions.DependencyInjection;
1513
using ModelContextProtocol.Client;
14+
using ModelContextProtocol.Extensions.Tasks;
1615
using ModelContextProtocol.Protocol;
1716
using ModelContextProtocol.Server;
1817
using System.ComponentModel;
@@ -21,28 +20,25 @@
2120

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

24-
await using McpServer server = McpServer.Create(
25-
new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()),
26-
new McpServerOptions
27-
{
28-
// Setting TaskStore is all that's needed for [McpServerTool]-attributed tools to be
29-
// automatically wrapped as background tasks when the client opts in.
30-
TaskStore = new InMemoryMcpTaskStore { DefaultPollIntervalMs = 250 },
31-
ToolCollection = [McpServerTool.Create(SlowTools.RunReport, new() { Name = "run-report" })],
32-
});
23+
var store = new InMemoryMcpTaskStore { DefaultPollIntervalMs = 250 };
24+
25+
var services = new ServiceCollection();
26+
services.AddMcpServer()
27+
.WithTools([McpServerTool.Create(SlowTools.RunReport, new() { Name = "run-report" })])
28+
.WithTasks(store);
29+
services.AddSingleton<ITransport>(new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()));
30+
31+
await using var serviceProvider = services.BuildServiceProvider();
32+
var server = serviceProvider.GetRequiredService<McpServer>();
3333
_ = server.RunAsync();
3434

3535
await using McpClient client = await McpClient.CreateAsync(
36-
new StreamClientTransport(clientToServerPipe.Writer.AsStream(), serverToClientPipe.Reader.AsStream()));
37-
38-
Console.WriteLine("=== CallToolAsync (auto-poll) ===");
39-
var auto = await client.CallToolAsync(
40-
new CallToolRequestParams { Name = "run-report" });
41-
Console.WriteLine($" result: {((TextContentBlock)auto.Content[0]).Text}");
42-
Console.WriteLine();
36+
new StreamClientTransport(
37+
serverInput: clientToServerPipe.Writer.AsStream(),
38+
serverOutput: serverToClientPipe.Reader.AsStream()));
4339

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

9086
case InputRequiredTaskResult inputRequired:
91-
// The auto-poll path (CallToolAsync above) routes these through the registered
92-
// ElicitationHandler/SamplingHandler automatically. The manual path needs to call
93-
// UpdateTaskAsync with responses for each outstanding key.
9487
Console.WriteLine($" poll {pollCount}: input requested ({inputRequired.InputRequests?.Count ?? 0} key(s))");
9588
continue;
9689
}

samples/TasksExtension/TasksExtension.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
<ItemGroup>
1212
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
13+
<ProjectReference Include="..\..\src\ModelContextProtocol.Extensions.Tasks\ModelContextProtocol.Extensions.Tasks.csproj" />
14+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
1315
</ItemGroup>
1416

1517
</Project>

0 commit comments

Comments
 (0)