Context
SEP-2663 (Tasks extension) is being implemented in #1579 and SEP-2322 (MRTR) landed in #1458. Both extensions let a tool ask the user for input mid-execution, but via different protocol mechanisms:
- MRTR:
tools/call round-trips — the server returns an input_required result, the client re-sends tools/call with input responses, the server replays the handler.
- Tasks:
tasks/update — the server sets the task to status = input_required with the pending input requests, the client sends tasks/update with input responses, the task body's ElicitAsync/SampleAsync/RequestRootsAsync await resumes.
End users writing [McpServerTool] methods don't know (and shouldn't need to know) which mechanism the client is using. Today, after #1579 lands, two composition cases break:
Case 1: [McpServerTool] + tasks _meta opt-in + input requests inside the method body
A [McpServerTool] method that calls ElicitAsync/SampleAsync/RequestRootsAsync works when invoked synchronously (MRTR translates the call into a tools/call round-trip). But when the client signals the tasks opt-in (SEP-2663 §51 _meta envelope), the SDK pre-creates a task and runs the method body in Task.Run. Inside the body, the MRTR backcompat resolver throws InputRequiredException. The task wrapper's generic catch (Exception ex) at McpServerImpl.cs:933 turns this into a Failed task with the literal exception message — no hint that "MRTR can't compose with tasks under this wrapper".
The SEP-1686-era SDK supported this case via the AutomaticInputRequiredStatusTests test class (now deleted in cec5d998), which asserted that ElicitAsync/SampleAsync inside a task body auto-transitioned the task to InputRequired status. SEP-2663 preserves this capability at the protocol level (InputRequiredTaskResult + UpdateTaskRequestParams.InputResponses) but the new task wrapper doesn't wire it through.
Case 2: Sync [McpServerTool] that needs to escalate to a task mid-execution
A [McpServerTool] method may run synchronously for "most" requests but occasionally need to do long-running work. Today, the only way to support this is to write a CallToolWithTaskHandler that drives the task lifecycle manually. The pre-#1458 SDK had a DeferTaskCreation opt-in on [McpServerTool] that I removed in #1458 to keep the MRTR PR minimal — so this capability is currently absent.
Restoring something like DeferTaskCreation would let the method run sync first, then call e.g. context.PromoteToTaskAsync() to create a task and detach.
Proposed approach
Design and implement a unified composition story that addresses both cases. Sketch (not prescriptive — the right design needs more thought):
-
Task-aware MRTR for Case 1. When MRTR sees that it's running inside a task scope (via McpTaskExecutionContext, which already exists at src/ModelContextProtocol.Core/Server/McpTaskExecutionContext.cs), translate ElicitAsync/SampleAsync/RequestRootsAsync into task-protocol input requests (taskStore.SetInputRequestsAsync(...) and await tasks/update resume) instead of MRTR round-trips. The deleted AutomaticInputRequiredStatusTests is the behavioral contract to restore.
-
DeferTaskCreation for Case 2. Re-introduce the [McpServerTool(DeferTaskCreation = true)] opt-in (or an equivalent McpServerToolCreateOptions shape) plus a context API (context.PromoteToTaskAsync(McpTaskInfo?) or similar) that the method can call to escalate.
-
Documentation in docs/concepts/tasks/tasks.md covering both patterns with runnable examples.
Open design questions:
- Should
DeferTaskCreation be the default for async [McpServerTool] methods, or stay opt-in? (Default-on is more ergonomic but changes existing behavior.)
- How should
McpTaskExecutionContext be discovered from within MRTR — AsyncLocal, parameter injection, or something else?
- What does Case 1 look like when the client supports neither the tasks extension nor MRTR? (Probably: today's behavior, error returned.)
Out of scope
Related
Context
SEP-2663 (Tasks extension) is being implemented in #1579 and SEP-2322 (MRTR) landed in #1458. Both extensions let a tool ask the user for input mid-execution, but via different protocol mechanisms:
tools/callround-trips — the server returns aninput_requiredresult, the client re-sendstools/callwith input responses, the server replays the handler.tasks/update— the server sets the task tostatus = input_requiredwith the pending input requests, the client sendstasks/updatewith input responses, the task body'sElicitAsync/SampleAsync/RequestRootsAsyncawait resumes.End users writing
[McpServerTool]methods don't know (and shouldn't need to know) which mechanism the client is using. Today, after #1579 lands, two composition cases break:Case 1:
[McpServerTool]+ tasks_metaopt-in + input requests inside the method bodyA
[McpServerTool]method that callsElicitAsync/SampleAsync/RequestRootsAsyncworks when invoked synchronously (MRTR translates the call into atools/callround-trip). But when the client signals the tasks opt-in (SEP-2663 §51_metaenvelope), the SDK pre-creates a task and runs the method body inTask.Run. Inside the body, the MRTR backcompat resolver throwsInputRequiredException. The task wrapper's genericcatch (Exception ex)atMcpServerImpl.cs:933turns this into aFailedtask with the literal exception message — no hint that "MRTR can't compose with tasks under this wrapper".The SEP-1686-era SDK supported this case via the
AutomaticInputRequiredStatusTeststest class (now deleted incec5d998), which asserted thatElicitAsync/SampleAsyncinside a task body auto-transitioned the task toInputRequiredstatus. SEP-2663 preserves this capability at the protocol level (InputRequiredTaskResult+UpdateTaskRequestParams.InputResponses) but the new task wrapper doesn't wire it through.Case 2: Sync
[McpServerTool]that needs to escalate to a task mid-executionA
[McpServerTool]method may run synchronously for "most" requests but occasionally need to do long-running work. Today, the only way to support this is to write aCallToolWithTaskHandlerthat drives the task lifecycle manually. The pre-#1458 SDK had aDeferTaskCreationopt-in on[McpServerTool]that I removed in #1458 to keep the MRTR PR minimal — so this capability is currently absent.Restoring something like
DeferTaskCreationwould let the method run sync first, then call e.g.context.PromoteToTaskAsync()to create a task and detach.Proposed approach
Design and implement a unified composition story that addresses both cases. Sketch (not prescriptive — the right design needs more thought):
Task-aware MRTR for Case 1. When MRTR sees that it's running inside a task scope (via
McpTaskExecutionContext, which already exists atsrc/ModelContextProtocol.Core/Server/McpTaskExecutionContext.cs), translateElicitAsync/SampleAsync/RequestRootsAsyncinto task-protocol input requests (taskStore.SetInputRequestsAsync(...)and awaittasks/updateresume) instead of MRTR round-trips. The deletedAutomaticInputRequiredStatusTestsis the behavioral contract to restore.DeferTaskCreationfor Case 2. Re-introduce the[McpServerTool(DeferTaskCreation = true)]opt-in (or an equivalentMcpServerToolCreateOptionsshape) plus a context API (context.PromoteToTaskAsync(McpTaskInfo?)or similar) that the method can call to escalate.Documentation in
docs/concepts/tasks/tasks.mdcovering both patterns with runnable examples.Open design questions:
DeferTaskCreationbe the default for async[McpServerTool]methods, or stay opt-in? (Default-on is more ergonomic but changes existing behavior.)McpTaskExecutionContextbe discovered from within MRTR —AsyncLocal, parameter injection, or something else?Out of scope
DeferTaskCreation—IMcpTaskStorealready abstracts this.Related
DeferTaskCreationto keep scope minimal)_metaopt-in envelope), §306 (durability), §186 (failed.error shape)tests/ModelContextProtocol.Tests/Server/AutomaticInputRequiredStatusTests.csremoved in commitcec5d998