{"slug": "mcp-c-task-polling-stop-infinite-input-required-loops", "title": "MCP C# Task Polling: Stop Infinite input_required Loops", "summary": "The MCP C# SDK's Tasks extension provides a bounded guard against infinite `input_required` polling loops, according to a developer's sample. By setting `maxConsecutiveStuckPolls`, clients can detect when a server returns no new input-request keys, trigger a `tasks/cancel`, and throw an `McpException`. The developer's verifier demonstrates the exact behavior with a stuck limit of three, ensuring deterministic testing without external dependencies.", "body_md": "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`\n\nrequest. 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.\n\nThe stable C# Tasks extension already provides that guard. The key is to set `maxConsecutiveStuckPolls`\n\ndeliberately and test what happens when a server never advances.\n\nIn the MCP `2026-07-28`\n\nTasks extension, a tool call can return a task instead of its final tool result. The client then uses `tasks/get`\n\nuntil the task completes, fails, is cancelled, or asks for input.\n\nAn `input_required`\n\nresult contains keyed requests. A simplified response looks like this:\n\n```\n{\n  \"taskId\": \"task-1\",\n  \"status\": \"input_required\",\n  \"pollInterval\": 1,\n  \"inputRequests\": {\n    \"approval\": {\n      \"method\": \"elicitation/create\"\n    }\n  }\n}\n```\n\nThe key matters. If the next poll returns `approval`\n\nagain, 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.\n\nThe official [C# SDK Tasks guide](https://csharp.sdk.modelcontextprotocol.io/v2/concepts/tasks/tasks.html) says `CallToolWithPollingAsync`\n\ndeduplicates input-request keys. It also detects repeated `input_required`\n\npolls that contain no new keys, makes a best-effort `tasks/cancel`\n\ncall, and throws `McpException`\n\n. The default stuck-poll threshold is 60.\n\nThe extension method keeps the policy close to the call:\n\n```\ntry\n{\n    CallToolResult result = await client.CallToolWithPollingAsync(\n        new CallToolRequestParams { Name = \"long-running-tool\" },\n        maxConsecutiveStuckPolls: 3,\n        cancellationToken: cancellationToken);\n\n    // Consume the completed tool result.\n}\ncatch (McpException exception)\n{\n    logger.LogWarning(exception, \"MCP task stopped making progress\");\n}\n```\n\nI use `3`\n\nin 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.\n\nThis guard complements the caller's `CancellationToken`\n\n. 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.\n\nMy [complete sample on main](https://github.com/ssukhpinder/dev-to-code-samples/tree/main/111-mcp-stuck-task-polling) uses\n\n`ModelContextProtocol.Extensions.Tasks`\n\n2.2.0 and an in-memory transport. It advertises MCP `2026-07-28`\n\n, returns a task from `tools/call`\n\n, and then returns the same `approval`\n\nrequest key on every `tasks/get`\n\ncall.The elicitation handler declines the request and counts how often it runs:\n\n```\nHandlers = new McpClientHandlers\n{\n    ElicitationHandler = (request, cancellationToken) =>\n    {\n        elicitationCalls++;\n        return ValueTask.FromResult(\n            new ElicitResult { Action = \"decline\" });\n    },\n};\n```\n\nWith a stuck limit of three, the observed contract is precise:\n\n`approval`\n\nkey.`tasks/update`\n\ncontaining `approval: decline`\n\nfor `task-1`\n\n.`tasks/get`\n\ncalls: one that introduces the key, then three with no new key.`tasks/cancel`\n\nfor `task-1`\n\n.`McpException`\n\ninstead 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.\n\nThe 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`\n\nextension model; the [official release notes](https://blog.modelcontextprotocol.io/posts/2026-07-28/) explain the poll-based lifecycle.\n\nA 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.\n\nCancellation is cooperative and eventually consistent. A successful `tasks/cancel`\n\nresponse does not prove that remote work stopped at that exact instant, so callers must tolerate a late state transition and avoid assuming rollback.\n\nThis 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`\n\nrepeats without a new key. Log task IDs and state transitions for diagnosis, but keep elicitation answers and credentials out of logs.\n\nWhat stuck-poll threshold fits your server's poll interval and expected human response time?\n\nHappy coding!", "url": "https://wpnews.pro/news/mcp-c-task-polling-stop-infinite-input-required-loops", "canonical_source": "https://dev.to/ssukhpinder/mcp-c-task-polling-stop-infinite-inputrequired-loops-3nnk", "published_at": "2026-08-31 02:15:50+00:00", "updated_at": "2026-08-31 02:51:35.244131+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": ["ModelContextProtocol", "C# SDK", "McpException", "CallToolWithPollingAsync"], "alternates": {"html": "https://wpnews.pro/news/mcp-c-task-polling-stop-infinite-input-required-loops", "markdown": "https://wpnews.pro/news/mcp-c-task-polling-stop-infinite-input-required-loops.md", "text": "https://wpnews.pro/news/mcp-c-task-polling-stop-infinite-input-required-loops.txt", "jsonld": "https://wpnews.pro/news/mcp-c-task-polling-stop-infinite-input-required-loops.jsonld"}}