Skip to content

Restore [McpServerTool] composability with MRTR input requests + tasks #1635

Description

@halter73

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):

  1. 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.

  2. 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.

  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions