modelcontextprotocol / modelcontextprotocol/csharp-sdk

Restore `[McpServerTool]` composability with MRTR input requests + tasks

Open
#1,635 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement P2 ready for work
Dominant language
C#
Stars
4.5k
Forks
814
Avg merge
9d 19h
Merged PRs (30d)
4

Description

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

  • Mid-flight transport switching (sticky session vs. resumable HTTP) — orthogonal, blocked on SEP-2575/2567 / #1610.
  • Cross-process task store handoff for DeferTaskCreationIMcpTaskStore already abstracts this.

Related

  • #1458 — MRTR (removed the original DeferTaskCreation to keep scope minimal)
  • #1579 — SEP-2663 Tasks extension
  • #1610 — sessionless + handshake-less draft protocol (SEP-2575 + SEP-2567)
  • SEP-2663 §51 (per-request _meta opt-in envelope), §306 (durability), §186 (failed.error shape)
  • Deleted test (SEP-1686-era behavior contract for Case 1): tests/ModelContextProtocol.Tests/Server/AutomaticInputRequiredStatusTests.cs removed in commit cec5d998

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/ModelContextProtocol.Core/Server/McpTaskExecutionContext.cs, the task wrapper around McpServerImpl.cs:933, and the deleted tests/ModelContextProtocol.Tests/Server/AutomaticInputRequiredStatusTests.cs to understand the existing behavior. Define and validate task-aware MRTR input handling and synchronous task promotion, then document both patterns with runnable examples in docs/concepts/tasks/tasks.md.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.