Skip to content

Commit 9776eed

Browse files
jeffhandleyCopilot
andcommitted
Migrate Tasks tests and sample to extension package
- Add ProjectReference from tests and sample to ModelContextProtocol.Extensions.Tasks - Migrate task tests to public extension API (WithTasks, CallToolAsTaskAsync, GetTaskAsync, UpdateTaskAsync, CancelTaskAsync, CallToolRawAsync) - Rename const references to TaskMethods.Get/Update/Cancel/StatusNotification - Rewrite McpServerTaskTests and TaskPollStuckDetectorTests to use the store-based public API with custom IMcpTaskStore doubles - Drop manual CallToolWithTaskHandler tests (TaskStoreOrphanedTaskTests, TaskHandlerConfigurationValidationTests) that exercised configuration paths removed from Core - Add public McpTasksJsonUtilities.DefaultOptions so the public task DTOs can be serialized under source generation when reflection is disabled - Point task DTO serialization in tests at McpTasksJsonUtilities.DefaultOptions instead of the Core McpJsonUtilities.DefaultOptions - Send and read tasks/update inputResponses explicitly in the extension because RequestParams serializes them through an internal backing property the extension context cannot access - Update samples/TasksExtension to the WithTasks plus CallToolAsTaskAsync API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 16690b1 commit 9776eed

17 files changed

Lines changed: 306 additions & 759 deletions

samples/TasksExtension/Program.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// is self-contained — no separate server process or HTTP transport required.
1414

1515
using ModelContextProtocol.Client;
16+
using ModelContextProtocol.Extensions.Tasks;
1617
using ModelContextProtocol.Protocol;
1718
using ModelContextProtocol.Server;
1819
using System.ComponentModel;
@@ -21,22 +22,25 @@
2122

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

25+
McpServerOptions serverOptions = new()
26+
{
27+
ToolCollection = [McpServerTool.Create(SlowTools.RunReport, new() { Name = "run-report" })],
28+
};
29+
30+
// Calling WithTasks is all that's needed for [McpServerTool]-attributed tools to be
31+
// automatically wrapped as background tasks when the client opts in.
32+
serverOptions.WithTasks(new InMemoryMcpTaskStore { DefaultPollIntervalMs = 250 });
33+
2434
await using McpServer server = McpServer.Create(
2535
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-
});
36+
serverOptions);
3337
_ = server.RunAsync();
3438

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

38-
Console.WriteLine("=== CallToolAsync (auto-poll) ===");
39-
var auto = await client.CallToolAsync(
42+
Console.WriteLine("=== CallToolAsTaskAsync (auto-poll) ===");
43+
var auto = await client.CallToolAsTaskAsync(
4044
new CallToolRequestParams { Name = "run-report" });
4145
Console.WriteLine($" result: {((TextContentBlock)auto.Content[0]).Text}");
4246
Console.WriteLine();

samples/TasksExtension/TasksExtension.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
<TargetFramework>net8.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
8-
<NoWarn>$(NoWarn);MCPEXP001</NoWarn>
8+
<NoWarn>$(NoWarn);MCPEXP001;MCPEXP002;MCPEXP004</NoWarn>
99
</PropertyGroup>
1010

1111
<ItemGroup>
1212
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
13+
<ProjectReference Include="..\..\src\ModelContextProtocol.Extensions.Tasks\ModelContextProtocol.Extensions.Tasks.csproj" />
1314
</ItemGroup>
1415

1516
</Project>

src/ModelContextProtocol.Extensions.Tasks/Client/McpClientTasksExtensions.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using ModelContextProtocol.Client;
22
using ModelContextProtocol.Protocol;
3-
using System.Diagnostics.CodeAnalysis;
43
using System.Text.Json;
54
using System.Text.Json.Nodes;
65

@@ -11,9 +10,9 @@ namespace ModelContextProtocol.Extensions.Tasks;
1110
/// </summary>
1211
/// <remarks>
1312
/// These methods let a client request task-augmented tool execution and drive the task lifecycle
14-
/// (<c>tasks/get</c>, <c>tasks/update</c>, <c>tasks/cancel</c>). The Tasks extension is draft-only.
13+
/// (<c>tasks/get</c>, <c>tasks/update</c>, <c>tasks/cancel</c>). The Tasks extension requires the
14+
/// <c>2026-07-28</c> or later protocol revision.
1515
/// </remarks>
16-
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
1716
public static class McpClientTasksExtensions
1817
{
1918
/// <summary>The default number of consecutive stuck polls tolerated before a task is abandoned.</summary>
@@ -309,11 +308,19 @@ public static async ValueTask<UpdateTaskResult> UpdateTaskAsync(
309308
if (requestParams is null) throw new ArgumentNullException(nameof(requestParams));
310309
ThrowIfTasksNotSupported(client, nameof(UpdateTaskAsync));
311310

311+
// RequestParams serializes inputResponses via an internal backing property that this assembly's
312+
// source-generated context cannot access, so attach the inputResponses node explicitly.
313+
var paramsNode = JsonSerializer.SerializeToNode(requestParams, TasksJsonContext.Default.UpdateTaskRequestParams);
314+
if (requestParams.InputResponses is { Count: > 0 } responses && paramsNode is JsonObject paramsObject)
315+
{
316+
paramsObject["inputResponses"] = JsonSerializer.SerializeToNode(responses, TasksJsonContext.Default.IDictionaryStringInputResponse);
317+
}
318+
312319
var response = await client.SendRequestAsync(
313320
new JsonRpcRequest
314321
{
315322
Method = TaskMethods.Update,
316-
Params = JsonSerializer.SerializeToNode(requestParams, TasksJsonContext.Default.UpdateTaskRequestParams),
323+
Params = paramsNode,
317324
},
318325
cancellationToken).ConfigureAwait(false);
319326

@@ -396,9 +403,8 @@ private static void ThrowIfTasksNotSupported(McpClient client, string operationN
396403
if (!client.IsDraftProtocol())
397404
{
398405
throw new InvalidOperationException(
399-
$"'{operationName}' requires the draft protocol revision. " +
400-
$"The negotiated protocol version is '{client.NegotiatedProtocolVersion ?? "(none)"}'. " +
401-
"The Tasks extension is only available under the draft revision.");
406+
$"'{operationName}' requires a newer protocol revision that supports tasks. " +
407+
$"The negotiated protocol version is '{client.NegotiatedProtocolVersion ?? "(none)"}'.");
402408
}
403409
}
404410
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Text.Json;
2+
3+
namespace ModelContextProtocol.Extensions.Tasks;
4+
5+
/// <summary>
6+
/// Provides serialization helpers for the SEP-2663 Tasks extension protocol types.
7+
/// </summary>
8+
public static class McpTasksJsonUtilities
9+
{
10+
/// <summary>
11+
/// Gets the <see cref="JsonSerializerOptions"/> used to serialize and deserialize the
12+
/// Tasks extension protocol types (for example <see cref="ModelContextProtocol.Protocol.CreateTaskResult"/>
13+
/// and <see cref="ModelContextProtocol.Protocol.GetTaskResult"/>).
14+
/// </summary>
15+
/// <remarks>
16+
/// These options are backed by a source-generated <see cref="System.Text.Json.Serialization.JsonSerializerContext"/>,
17+
/// so they are safe to use when reflection-based serialization is disabled (for example under Native AOT).
18+
/// </remarks>
19+
public static JsonSerializerOptions DefaultOptions { get; } = TasksJsonContext.Default.Options;
20+
}

src/ModelContextProtocol.Extensions.Tasks/Server/McpServerTasksExtensions.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using ModelContextProtocol.Protocol;
44
using ModelContextProtocol.Server;
55
using System.Collections.Concurrent;
6-
using System.Diagnostics.CodeAnalysis;
76
using System.Text.Json;
87
using System.Text.Json.Nodes;
98

@@ -17,7 +16,6 @@ namespace ModelContextProtocol.Extensions.Tasks;
1716
/// client poll for completion via <c>tasks/get</c>, supply input via <c>tasks/update</c>, and cancel via
1817
/// <c>tasks/cancel</c>. State is held in an <see cref="IMcpTaskStore"/>.
1918
/// </remarks>
20-
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
2119
public static class McpServerTasksExtensions
2220
{
2321
/// <summary>
@@ -113,7 +111,18 @@ private static void ConfigureTaskHandlers(IMcpServerRawHandlerRegistry registry,
113111
EnsureDraft(registry, request, TaskMethods.Update);
114112

115113
var requestParams = Deserialize(request.Params, TasksJsonContext.Default.UpdateTaskRequestParams);
116-
var inputResponses = requestParams.InputResponses ?? new Dictionary<string, InputResponse>();
114+
115+
// RequestParams deserializes inputResponses via an internal backing property that this assembly's
116+
// source-generated context cannot access, so read the inputResponses node explicitly.
117+
IDictionary<string, InputResponse> inputResponses = new Dictionary<string, InputResponse>();
118+
if (request.Params is JsonObject paramsObject &&
119+
paramsObject.TryGetPropertyValue("inputResponses", out var inputResponsesNode) &&
120+
inputResponsesNode is not null)
121+
{
122+
inputResponses = JsonSerializer.Deserialize(inputResponsesNode, TasksJsonContext.Default.IDictionaryStringInputResponse)
123+
?? inputResponses;
124+
}
125+
117126
await store.ResolveInputRequestsAsync(requestParams.TaskId, inputResponses, cancellationToken).ConfigureAwait(false);
118127

119128
return JsonSerializer.SerializeToNode(new UpdateTaskResult(), TasksJsonContext.Default.UpdateTaskResult);

tests/Directory.Build.props

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
66
<!-- Suppress the experimental extensions warning in test projects -->
77
<NoWarn>$(NoWarn);MCPEXP001</NoWarn>
8+
<!-- Suppress the experimental SDK extensibility seam warning (raw handler registry, outgoing-request
9+
interceptor, draft/input-resolution primitives) in test projects. -->
10+
<NoWarn>$(NoWarn);MCPEXP002</NoWarn>
11+
<!-- Suppress the experimental Tasks extension warning in test projects. -->
12+
<NoWarn>$(NoWarn);MCPEXP004</NoWarn>
813
<!-- Suppress the obsolete SSE warning in test projects that need to test legacy SSE -->
914
<NoWarn>$(NoWarn);MCP9004</NoWarn>
1015
<!-- Suppress the Roots, Sampling, and Logging obsoletion warning (SEP-2577) in test projects

tests/ModelContextProtocol.Tests/Client/McpClientTaskMethodsTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using ModelContextProtocol.Client;
3+
using ModelContextProtocol.Extensions.Tasks;
34
using ModelContextProtocol.Protocol;
45
using ModelContextProtocol.Server;
56
using System.Runtime.InteropServices;
@@ -27,10 +28,10 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
2728
{
2829
mcpServerBuilder.Services.Configure<McpServerOptions>(options =>
2930
{
30-
options.TaskStore = new InMemoryMcpTaskStore
31+
options.WithTasks(new InMemoryMcpTaskStore
3132
{
3233
DefaultPollIntervalMs = 50,
33-
};
34+
});
3435
});
3536

3637
mcpServerBuilder.WithTools([McpServerTool.Create(

tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
8484
<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj" />
8585
<ProjectReference Include="..\..\src\ModelContextProtocol.Extensions.Apps\ModelContextProtocol.Extensions.Apps.csproj" />
86+
<ProjectReference Include="..\..\src\ModelContextProtocol.Extensions.Tasks\ModelContextProtocol.Extensions.Tasks.csproj" />
8687
<ProjectReference Include="..\ModelContextProtocol.TestServer\ModelContextProtocol.TestServer.csproj" />
8788
<ProjectReference Include="..\..\samples\TestServerWithHosting\TestServerWithHosting.csproj" />
8889
</ItemGroup>

0 commit comments

Comments
 (0)