Skip to content

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

@halter73

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions