# MCP C# Task Polling: Stop Infinite input_required Loops

> Source: <https://dev.to/ssukhpinder/mcp-c-task-polling-stop-infinite-inputrequired-loops-3nnk>
> Published: 2026-08-31 02:15:50+00:00

MCP Tasks let a tool finish asynchronously, but they also introduce a failure mode that a normal request timeout does not describe well: the task is alive, yet every poll returns the same `input_required`

request. For MCP C# task polling, I want a bounded definition of “no progress,” not an endless loop or a user prompt that appears again and again.

The stable C# Tasks extension already provides that guard. The key is to set `maxConsecutiveStuckPolls`

deliberately and test what happens when a server never advances.

In the MCP `2026-07-28`

Tasks extension, a tool call can return a task instead of its final tool result. The client then uses `tasks/get`

until the task completes, fails, is cancelled, or asks for input.

An `input_required`

result contains keyed requests. A simplified response looks like this:

```
{
  "taskId": "task-1",
  "status": "input_required",
  "pollInterval": 1,
  "inputRequests": {
    "approval": {
      "method": "elicitation/create"
    }
  }
}
```

The key matters. If the next poll returns `approval`

again, it is not a new question. Presenting it twice can produce duplicate confirmations or conflicting responses. Polling forever is not better; it hides a server that has stopped making useful progress.

The official [C# SDK Tasks guide](https://csharp.sdk.modelcontextprotocol.io/v2/concepts/tasks/tasks.html) says `CallToolWithPollingAsync`

deduplicates input-request keys. It also detects repeated `input_required`

polls that contain no new keys, makes a best-effort `tasks/cancel`

call, and throws `McpException`

. The default stuck-poll threshold is 60.

The extension method keeps the policy close to the call:

```
try
{
    CallToolResult result = await client.CallToolWithPollingAsync(
        new CallToolRequestParams { Name = "long-running-tool" },
        maxConsecutiveStuckPolls: 3,
        cancellationToken: cancellationToken);

    // Consume the completed tool result.
}
catch (McpException exception)
{
    logger.LogWarning(exception, "MCP task stopped making progress");
}
```

I use `3`

in a fast verifier, not as a universal production value. The practical time bound is approximately the threshold multiplied by the server's poll interval. A task polled every second and a task polled every 30 seconds should not automatically share the same threshold.

This guard complements the caller's `CancellationToken`

. Caller cancellation answers “does my operation still need this result?” The stuck-poll guard answers “is the server returning any new work or state?” Those are different decisions, and keeping both makes the failure easier to diagnose.

My [complete sample on main](https://github.com/ssukhpinder/dev-to-code-samples/tree/main/111-mcp-stuck-task-polling) uses

`ModelContextProtocol.Extensions.Tasks`

2.2.0 and an in-memory transport. It advertises MCP `2026-07-28`

, returns a task from `tools/call`

, and then returns the same `approval`

request key on every `tasks/get`

call.The elicitation handler declines the request and counts how often it runs:

```
Handlers = new McpClientHandlers
{
    ElicitationHandler = (request, cancellationToken) =>
    {
        elicitationCalls++;
        return ValueTask.FromResult(
            new ElicitResult { Action = "decline" });
    },
};
```

With a stuck limit of three, the observed contract is precise:

`approval`

key.`tasks/update`

containing `approval: decline`

for `task-1`

.`tasks/get`

calls: one that introduces the key, then three with no new key.`tasks/cancel`

for `task-1`

.`McpException`

instead of polling again.The verifier runs twice and compares the output byte for byte. It requires no MCP host, model account, credentials, clock, random values, or runtime network access. The [merged pull request](https://github.com/ssukhpinder/dev-to-code-samples/pull/101) also records the exact restore, format, build, run, package, and vulnerability-audit commands.

The package used here is the stable [ModelContextProtocol.Extensions.Tasks 2.2.0 release](https://www.nuget.org/packages/ModelContextProtocol.Extensions.Tasks/2.2.0). Tasks are part of the final MCP `2026-07-28`

extension model; the [official release notes](https://blog.modelcontextprotocol.io/posts/2026-07-28/) explain the poll-based lifecycle.

A low threshold is useful in a unit test because it turns a potential hang into a quick deterministic failure. In production, the same value could cancel a healthy task while a person is considering an approval prompt. I would choose it from the expected poll interval, normal response latency, and the cost of leaving remote work active.

Cancellation is cooperative and eventually consistent. A successful `tasks/cancel`

response does not prove that remote work stopped at that exact instant, so callers must tolerate a late state transition and avoid assuming rollback.

This pattern is also not a replacement for an overall deadline, retry policy, or server-side task expiry. It specifically protects the polling loop when `input_required`

repeats without a new key. Log task IDs and state transitions for diagnosis, but keep elicitation answers and credentials out of logs.

What stuck-poll threshold fits your server's poll interval and expected human response time?

Happy coding!
