Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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