Skip to content

Commit 9eee757

Browse files
jeffhandleyCopilot
andcommitted
Extract Tasks into ModelContextProtocol.Extensions.Tasks package
- Create src/ModelContextProtocol.Extensions.Tasks/ with csproj, JSON context, server builder extensions, client extension methods - Move task protocol DTOs (CreateTaskResult, GetTaskResult, UpdateTask*, CancelTask*, McpTaskStatus, TaskStatusNotificationParams) to extension - Move server types (IMcpTaskStore, InMemoryMcpTaskStore, McpTaskInfo, InputResponseReceivedEventArgs) to extension - Move task constants (RequestMethods.Tasks*, NotificationMethods.TaskStatus*, MetaKeys.RelatedTask, McpExtensions.Tasks) into TasksProtocol static class - Delete McpTaskExecutionContext, ResultOrCreatedTask from Core - Remove ~18 task [JsonSerializable] entries from McpJsonUtilities - Remove McpServerOptions.TaskStore and McpClientOptions.MaxConsecutiveStuckPolls - Remove task client methods from McpClient (CallToolRawAsync, PollTaskToCompletion, GetTaskAsync, UpdateTaskAsync, CancelTaskAsync, GetMetaWithTaskCapability, ThrowIfTasksNotSupported) - Remove task server methods/handlers (GetTaskHandler, UpdateTaskHandler, CancelTaskHandler, ConfigureTasks, InvokeToolAsTask, task cancellation sources) - Add Tasks_DiagnosticId (MCPEXP001) to Experimentals.cs - Extension WithTasks(store) registers request handlers via seam #1, alternate filter via seam #2, interceptor via seam #3 - Extension client methods: CallToolAsTaskAsync, CallToolWithPollingAsync, GetTaskAsync, UpdateTaskAsync, CancelTaskAsync - Manually serialize/deserialize InputResponses in UpdateTaskAsync to handle internal Core property visibility across assembly boundary - Update test project and TasksExtension sample to reference new package - Update all task test files with new using and extension API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b5195b4 commit 9eee757

53 files changed

Lines changed: 1321 additions & 1394 deletions

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/">

samples/TasksExtension/Program.cs

Lines changed: 21 additions & 33 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,32 +20,27 @@
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
{
48-
// Either the server doesn't advertise the tasks extension or it chose to run the call
49-
// synchronously despite the client opt-in. Surface the inline result and stop.
5044
Console.WriteLine($" result (inline): {((TextContentBlock)raw.Result!.Content[0]).Text}");
5145
return;
5246
}
@@ -70,7 +64,6 @@
7064
switch (state)
7165
{
7266
case CompletedTaskResult completed:
73-
// The Result property carries the wrapped CallToolResult as a raw JsonElement.
7467
var callToolResult = completed.Result.Deserialize<CallToolResult>()!;
7568
Console.WriteLine($" task completed after {pollCount} poll(s): {((TextContentBlock)callToolResult.Content[0]).Text}");
7669
return;
@@ -88,9 +81,6 @@
8881
continue;
8982

9083
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.
9484
Console.WriteLine($" poll {pollCount}: input requested ({inputRequired.InputRequests?.Count ?? 0} key(s))");
9585
continue;
9686
}
@@ -101,8 +91,6 @@ internal static class SlowTools
10191
[Description("Runs a short simulated report and returns when it's done.")]
10292
public static async Task<string> RunReport(CancellationToken cancellationToken)
10393
{
104-
// Real-world workloads would do meaningful work here; we just sleep so the polling
105-
// path is observable in the console output.
10694
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
10795
return "report ready";
10896
}

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)