|
1 | 1 | // Demonstrates the MCP tasks extension (SEP-2663): |
2 | 2 | // |
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 |
4 | 4 | // 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. |
11 | 6 | // |
12 | 7 | // Both server and client are wired together in-process over an in-memory pipe so the sample |
13 | 8 | // is self-contained — no separate server process or HTTP transport required. |
14 | 9 |
|
| 10 | +#pragma warning disable MCPEXP001, MCPEXP002, MCPEXP004 |
| 11 | + |
| 12 | +using Microsoft.Extensions.DependencyInjection; |
15 | 13 | using ModelContextProtocol.Client; |
| 14 | +using ModelContextProtocol.Extensions.Tasks; |
16 | 15 | using ModelContextProtocol.Protocol; |
17 | 16 | using ModelContextProtocol.Server; |
18 | 17 | using System.ComponentModel; |
|
21 | 20 |
|
22 | 21 | Pipe clientToServerPipe = new(), serverToClientPipe = new(); |
23 | 22 |
|
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>(); |
33 | 33 | _ = server.RunAsync(); |
34 | 34 |
|
35 | 35 | 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())); |
43 | 39 |
|
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" }); |
46 | 42 | if (!raw.IsTask) |
47 | 43 | { |
48 | 44 | // Either the server doesn't advertise the tasks extension or it chose to run the call |
|
88 | 84 | continue; |
89 | 85 |
|
90 | 86 | 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. |
94 | 87 | Console.WriteLine($" poll {pollCount}: input requested ({inputRequired.InputRequests?.Count ?? 0} key(s))"); |
95 | 88 | continue; |
96 | 89 | } |
|
0 commit comments