Skip to content

Commit d917c80

Browse files
halter73Copilot
andcommitted
Add deferred task creation with DeferTaskCreation + CreateTaskAsync
Add support for tools to perform ephemeral MRTR exchanges before committing to a background task. This enables a two-phase workflow: 1. Ephemeral phase: The handler uses ElicitAsync/SampleAsync via MRTR to gather user input (e.g., confirmation before expensive operations). 2. Task phase: The handler calls CreateTaskAsync() to transition to a background task, receiving a task ID and cancellation token. API surface: - McpServerToolAttribute.DeferTaskCreation property - McpServerToolCreateOptions.DeferTaskCreation property - McpServerTool.DeferTaskCreation virtual property (overridden in AIFunctionMcpServerTool and DelegatingMcpServerTool) - McpServer.CreateTaskAsync() virtual method (overridden in DestinationBoundMcpServer) Implementation: - DeferredTaskInfo carries task metadata across MRTR continuations, with signal/ack TCS pair for handler ↔ framework coordination. - ConfigureTools attaches DeferredTaskInfo to MrtrContext when DeferTaskCreation is enabled and client provides task metadata. - AwaitMrtrHandlerAsync races handler vs exchange vs task creation signal (3-way WhenAny). - HandleDeferredTaskCreationAsync creates the task, re-links the handler CTS to the task cancellation token, and acknowledges the handler so it can continue as a background task. - TrackDeferredHandlerTaskAsync tracks completion and stores results (handler already tracked by ObserveHandlerCompletionAsync for in-flight counting). If the handler returns without calling CreateTaskAsync(), a normal (non-task) result is returned to the client. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d32295c commit d917c80

12 files changed

Lines changed: 725 additions & 4 deletions

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
1515
{
1616
private readonly bool _structuredOutputRequiresWrapping;
1717
private readonly IReadOnlyList<object> _metadata;
18+
private readonly bool _deferTaskCreation;
1819

1920
/// <summary>
2021
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
@@ -167,7 +168,7 @@ options.OpenWorld is not null ||
167168
tool.Execution.TaskSupport = ToolTaskSupport.Optional;
168169
}
169170

170-
return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping, options?.Metadata ?? []);
171+
return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping, options?.Metadata ?? [], options?.DeferTaskCreation ?? false);
171172
}
172173

173174
private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options)
@@ -211,6 +212,11 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
211212
newOptions.Execution ??= new ToolExecution();
212213
newOptions.Execution.TaskSupport ??= taskSupport;
213214
}
215+
216+
if (toolAttr._deferTaskCreation is bool deferTaskCreation)
217+
{
218+
newOptions.DeferTaskCreation = deferTaskCreation;
219+
}
214220
}
215221

216222
if (method.GetCustomAttribute<DescriptionAttribute>() is { } descAttr)
@@ -228,7 +234,7 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
228234
internal AIFunction AIFunction { get; }
229235

230236
/// <summary>Initializes a new instance of the <see cref="McpServerTool"/> class.</summary>
231-
private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider, bool structuredOutputRequiresWrapping, IReadOnlyList<object> metadata)
237+
private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider, bool structuredOutputRequiresWrapping, IReadOnlyList<object> metadata, bool deferTaskCreation)
232238
{
233239
ValidateToolName(tool.Name);
234240

@@ -237,11 +243,15 @@ private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider
237243

238244
_structuredOutputRequiresWrapping = structuredOutputRequiresWrapping;
239245
_metadata = metadata;
246+
_deferTaskCreation = deferTaskCreation;
240247
}
241248

242249
/// <inheritdoc />
243250
public override Tool ProtocolTool { get; }
244251

252+
/// <inheritdoc />
253+
public override bool DeferTaskCreation => _deferTaskCreation;
254+
245255
/// <inheritdoc />
246256
public override IReadOnlyList<object> Metadata => _metadata;
247257

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using ModelContextProtocol.Protocol;
2+
3+
namespace ModelContextProtocol.Server;
4+
5+
/// <summary>
6+
/// Contains the information the handler needs after the framework creates the deferred task.
7+
/// </summary>
8+
internal sealed class DeferredTaskCreationResult
9+
{
10+
/// <summary>Gets the ID of the created task.</summary>
11+
public required string TaskId { get; init; }
12+
13+
/// <summary>Gets the session ID associated with the task.</summary>
14+
public required string? SessionId { get; init; }
15+
16+
/// <summary>Gets the task store for persisting task state.</summary>
17+
public required IMcpTaskStore TaskStore { get; init; }
18+
19+
/// <summary>Gets whether to send task status notifications.</summary>
20+
public required bool SendNotifications { get; init; }
21+
22+
/// <summary>Gets the function for sending task status notifications.</summary>
23+
public required Func<McpTask, CancellationToken, Task>? NotifyTaskStatusFunc { get; init; }
24+
25+
/// <summary>Gets the cancellation token for the task (TTL-based or explicit).</summary>
26+
public required CancellationToken TaskCancellationToken { get; init; }
27+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using ModelContextProtocol.Protocol;
2+
3+
namespace ModelContextProtocol.Server;
4+
5+
/// <summary>
6+
/// Holds the state needed for deferred task creation, where a tool handler performs
7+
/// ephemeral MRTR exchanges before committing to a background task via
8+
/// <see cref="McpServer.CreateTaskAsync(CancellationToken)"/>.
9+
/// Stored on <see cref="MrtrContext.DeferredTask"/> and carried across MRTR continuations.
10+
/// </summary>
11+
internal sealed class DeferredTaskInfo
12+
{
13+
/// <summary>Gets the task metadata from the original client request.</summary>
14+
public required McpTaskMetadata TaskMetadata { get; init; }
15+
16+
/// <summary>Gets the JSON-RPC request ID of the current tools/call request.</summary>
17+
public required RequestId OriginalRequestId { get; init; }
18+
19+
/// <summary>Gets the original JSON-RPC request.</summary>
20+
public required JsonRpcRequest OriginalRequest { get; init; }
21+
22+
/// <summary>Gets the task store for persisting task state.</summary>
23+
public required IMcpTaskStore TaskStore { get; init; }
24+
25+
/// <summary>Gets whether to send task status notifications.</summary>
26+
public required bool SendNotifications { get; init; }
27+
28+
/// <summary>
29+
/// Task that completes when the handler calls <see cref="McpServer.CreateTaskAsync(CancellationToken)"/>.
30+
/// The framework races this against handler completion and MRTR exchanges.
31+
/// </summary>
32+
private readonly TaskCompletionSource<bool> _signalTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
33+
34+
/// <summary>
35+
/// TCS that the framework completes after creating the task, allowing the handler to continue.
36+
/// </summary>
37+
private readonly TaskCompletionSource<DeferredTaskCreationResult> _ackTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
38+
39+
/// <summary>Gets the task that completes when the handler requests task creation.</summary>
40+
public Task SignalTask => _signalTcs.Task;
41+
42+
/// <summary>
43+
/// Called by the handler (via <see cref="McpServer.CreateTaskAsync(CancellationToken)"/>) to signal
44+
/// the framework that a task should be created. Awaits the framework's acknowledgment.
45+
/// </summary>
46+
/// <returns>The result containing the created task's context information.</returns>
47+
/// <exception cref="InvalidOperationException"><see cref="McpServer.CreateTaskAsync(CancellationToken)"/> was already called.</exception>
48+
public async ValueTask<DeferredTaskCreationResult> RequestTaskCreationAsync(CancellationToken cancellationToken)
49+
{
50+
if (!_signalTcs.TrySetResult(true))
51+
{
52+
throw new InvalidOperationException("CreateTaskAsync has already been called for this tool execution.");
53+
}
54+
55+
return await _ackTcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
56+
}
57+
58+
/// <summary>
59+
/// Called by the framework after creating the task to unblock the handler.
60+
/// </summary>
61+
/// <exception cref="InvalidOperationException">Task creation was already acknowledged.</exception>
62+
public void AcknowledgeTaskCreation(DeferredTaskCreationResult result)
63+
{
64+
if (!_ackTcs.TrySetResult(result))
65+
{
66+
throw new InvalidOperationException("Task creation was already acknowledged.");
67+
}
68+
}
69+
70+
/// <summary>
71+
/// Called by the framework when task creation fails, propagating the exception
72+
/// to the handler so <see cref="McpServer.CreateTaskAsync(CancellationToken)"/> throws.
73+
/// </summary>
74+
public void AcknowledgeFailure(Exception exception)
75+
{
76+
_ackTcs.TrySetException(exception);
77+
}
78+
}

src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ protected DelegatingMcpServerTool(McpServerTool innerTool)
2323
/// <inheritdoc />
2424
public override Tool ProtocolTool => _innerTool.ProtocolTool;
2525

26+
/// <inheritdoc />
27+
public override bool DeferTaskCreation => _innerTool.DeferTaskCreation;
28+
2629
/// <inheritdoc />
2730
public override IReadOnlyList<object> Metadata => _innerTool.Metadata;
2831

src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,31 @@ private async Task<JsonRpcResponse> SendRequestViaMrtrAsync(
8989
Result = JsonSerializer.SerializeToNode(inputResponse.RawValue, McpJsonUtilities.JsonContext.Default.JsonElement),
9090
};
9191
}
92+
93+
/// <inheritdoc />
94+
public override async ValueTask CreateTaskAsync(CancellationToken cancellationToken = default)
95+
{
96+
var deferredTask = ActiveMrtrContext?.DeferredTask
97+
?? throw new InvalidOperationException(
98+
"CreateTaskAsync can only be called from a tool handler with DeferTaskCreation enabled " +
99+
"when the client provides task metadata in the tools/call request.");
100+
101+
// Signal the framework to create the task and wait for acknowledgment.
102+
// RequestTaskCreationAsync is atomic — throws if already called.
103+
var result = await deferredTask.RequestTaskCreationAsync(cancellationToken).ConfigureAwait(false);
104+
105+
// Transition to task mode on the handler's async flow.
106+
TaskExecutionContext.Current = new TaskExecutionContext
107+
{
108+
TaskId = result.TaskId,
109+
SessionId = result.SessionId,
110+
TaskStore = result.TaskStore,
111+
SendNotifications = result.SendNotifications,
112+
NotifyTaskStatusFunc = result.NotifyTaskStatusFunc,
113+
};
114+
115+
// No more ephemeral MRTR — subsequent ElicitAsync/SampleAsync calls
116+
// will go through SendRequestWithTaskStatusTrackingAsync instead.
117+
ActiveMrtrContext = null;
118+
}
92119
}

src/ModelContextProtocol.Core/Server/McpServer.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,40 @@ protected McpServer()
8383
[Experimental(Experimentals.Mrtr_DiagnosticId, UrlFormat = Experimentals.Mrtr_Url)]
8484
public virtual bool IsMrtrSupported => false;
8585

86+
/// <summary>
87+
/// Transitions the current tool execution from ephemeral MRTR mode to a background task.
88+
/// </summary>
89+
/// <remarks>
90+
/// <para>
91+
/// This method is only valid when called from a tool handler that has
92+
/// <see cref="McpServerToolAttribute.DeferTaskCreation"/> set to <see langword="true"/>
93+
/// and the client provided task metadata in the <c>tools/call</c> request.
94+
/// </para>
95+
/// <para>
96+
/// Before calling this method, <see cref="ElicitAsync(ElicitRequestParams, CancellationToken)"/>
97+
/// and <see cref="SampleAsync(CreateMessageRequestParams, CancellationToken)"/> use the ephemeral
98+
/// MRTR mechanism (returning <see cref="IncompleteResult"/> to the client). After calling this method,
99+
/// the task is created and subsequent calls use the persistent workflow (task status
100+
/// <see cref="McpTaskStatus.InputRequired"/> with <c>tasks/result</c> and <c>tasks/input_response</c>).
101+
/// </para>
102+
/// <para>
103+
/// If the tool handler returns without calling this method, a normal (non-task) result is returned
104+
/// to the client.
105+
/// </para>
106+
/// </remarks>
107+
/// <param name="cancellationToken">A token to cancel the task creation.</param>
108+
/// <exception cref="InvalidOperationException">
109+
/// The tool does not have <see cref="McpServerToolAttribute.DeferTaskCreation"/> enabled, or
110+
/// the client did not provide task metadata, or this method was already called.
111+
/// </exception>
112+
[Experimental(Experimentals.Tasks_DiagnosticId, UrlFormat = Experimentals.Tasks_Url)]
113+
public virtual ValueTask CreateTaskAsync(CancellationToken cancellationToken = default)
114+
{
115+
throw new InvalidOperationException(
116+
"CreateTaskAsync can only be called from a tool handler with DeferTaskCreation enabled " +
117+
"when the client provides task metadata in the tools/call request.");
118+
}
119+
86120
/// <summary>
87121
/// Runs the server, listening for and handling client requests.
88122
/// </summary>

0 commit comments

Comments
 (0)