Implement SEP-2663 Tasks Extension#1579
Conversation
# Conflicts: # docs/concepts/tasks/tasks.md # docs/list-of-diagnostics.md # src/ModelContextProtocol.Core/Client/McpClient.cs # src/ModelContextProtocol.Core/Client/McpClientImpl.cs
Reverts most of commit 18c0df7's removal of MrtrContext/MrtrContinuation/MrtrExchange, gating it to stateful sessions only. Tools calling ElicitAsync/SampleAsync/RequestRootsAsync under DRAFT-2026-v1 on stdio and stateful Streamable HTTP again transparently suspend the handler via TCS and emit InputRequiredResult to the client, with retries resumed via continuation lookup on requestState. Stateless Streamable HTTP still requires explicit InputRequiredException for MRTR: the WrapHandlerWithMrtr gate skips the implicit machinery when !IsStatefulSession() and routes through InvokeWithInputRequiredResultHandlingAsync, which already throws when the client doesn't support MRTR on stateless. Deferred-task related machinery (DeferredTask / DeferredTaskCreationResult / DeferTaskCreation / HandleDeferredTaskCreationAsync) is NOT restored. That work was superseded by SEP-2663 (PR #1579), which uses an entirely different API surface (McpServerOptions.TaskStore + per-request task metadata) and would just have to delete the restored SEP-1686 code during its rebase. Test coverage restored: MrtrIntegrationTests (Client), MrtrHandlerLifecycleTests / MrtrMessageFilterTests / MrtrSessionLimitTests (Server), plus the deleted SessionDelete_* + RetryWithInvalidRequestState_* tests in MrtrProtocolTests and the Mrtr_ParallelAwaits theory rows in MapMcpTests.Mrtr.cs. All previously wrapped methods (tools/call, prompts/get, resources/read) are wired through the MRTR interceptor again; updated InputRequiredResult XML doc accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merges upstream/main which includes the MRTR landing (SEP-2322, modelcontextprotocol#1458). Replaces the tasks PR's ad-hoc IDictionary<string, JsonElement> input-request/response envelopes with MRTR's typed InputRequest/InputResponse DTOs. Wire format is unchanged; this simply reuses the shared MRTR types across the tasks/get, tasks/update, and notifications/tasks paths so the two extensions share a single set of typed types. Conflict resolutions: - McpServerImpl.cs: combined task cancellation infrastructure with MRTR continuations; rethrow InputRequiredException from BuildInitialCallToolFilter/BuildInitialTaskToolFilter so the MRTR backcompat resolver (InvokeWithInputRequiredResultHandlingAsync) can catch it. - McpClientImpl.cs: collapsed the duplicate JsonElement-typed ResolveInputRequestsAsync into MRTR's typed override. - McpServer.Methods.cs: SendRequestViaTaskAsync now stores an InputRequest and unwraps InputResponse via Deserialize<T>(typeInfo). - UpdateTaskRequestParams: drops the redeclared InputResponses property and uses the inherited RequestParams.InputResponses from MRTR. - Result.cs: includes the 'task' resultType value alongside MRTR's 'complete' and 'input_required'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
halter73
left a comment
There was a problem hiding this comment.
I had an agent take a careful pass against SEP-2663, SEP-2575, and the MRTR landing in #1458. Overall this is in great shape. The MRTR integration is clean, the typed input-request/response flow reads well, and the roots/list dispatch you added in this revision closes the gap from our earlier conversation.
Highlights from the inline comments, in priority order:
- Wire-format
_metaenvelope (most important). SEP-2663 §51 / §58–62 says the per-request opt-in is the SEP-2575 envelope:_meta.io.modelcontextprotocol/clientCapabilities.extensions.io.modelcontextprotocol/tasks. We're currently writing and reading the bare_meta.io.modelcontextprotocol/taskskey on both sides, so two C# peers agree but a strictly-spec-shaped non-C# peer won't. Suggested change is symmetric on client (McpClient.Methods.cs) and server (McpServerImpl.cs); I'd also pair it with a small JSON-shape contract test (one that asserts the literal JSON path) so a future refactor can't silently regress to a bare key. Failedpayload structure +JsonDocumentlifetime. Two real bugs in the same five-linecatchblock inMcpServerImpl.cs: the failed payload only emits{"message": ...}(SEP-2663 §186 says it MUST be a JSON-RPC error object with at leastcode), and theJsonDocument.Parse(...).RootElementis neverusing-disposed so its pooled buffer can be recycled while the element is still in the store. Inline suggestion fixes both.- Roots/list tests. Production-side dispatch looks good — just needs a
RootsToolfixture next tosample-tooland oneRootsTool_ViaTask_RedirectsThroughStoretest mirroring the existing sample/elicit pair. Inline suggestions provided. - Two related task-composition failure modes — both should fail loudly instead of silently. A
CallToolWithTaskHandlerthat returnsIsTask = trueinside aTaskStorewrapper orphans the store's pre-created task (client polls a task that never completes); and anInputRequiredExceptionthrown from a[McpServerTool]inside a task wrapper becomes aFailedtask with a misleading message. Both fixes are a few lines — turn the orphan into aSetFailedAsyncwith a clear payload (Comment 5), and add a dedicatedcatch (InputRequiredException)that produces a "MRTR + tasks composition isn't supported under[McpServerTool]yet — useCallToolWithTaskHandler" error (Comment 9). I filed #1635 for the broader composability story as a follow-up. IMcpTaskStore.CreateTaskAsyncdocs. SEP-2663 §306 imposes a strong-consistency requirement (tasks/getMUST resolve immediately afterCreateTaskAsyncreturns). Worth adding a<remarks>paragraph so custom store implementers on eventually-consistent storage don't accidentally violate it.
Forward-compat notes from a parallel cross-check against #1610 (sessionless + handshake-less, SEP-2575/2567):
IMcpTaskStorelifetime under stateless HTTP. Once #1610 lands and flips HTTP toStateless = trueby default, each POST spins up a fresh server instance.CallToolAsync → CallToolRawAsync → PollTaskToCompletionAsyncissuestasks/getas fresh POSTs, soIMcpTaskStoreMUST be a singleton DI service (or backed by external storage) for the lookup to resolve across polls. Worth a sentence indocs/concepts/tasks/tasks.mdand a class-level remarks paragraph onIMcpTaskStoreitself.
Nit on the PR body: the "Known limitations" list still includes roots/list as input request — server emits, client doesn't dispatch yet, which is now stale given the dispatch you added.
Things I'd be happy to move to follow-up issues instead of holding this PR for:
- Replacement sample for the removed
samples/LongRunningTasks/(the new doc page covers the concepts, but a runnable sample is more useful for ecosystem adoption). - A test that pins down what happens when a client sends the tasks
_metaopt-in but the server has noTaskStoreconfigured (currently silent fallback to sync — fine if intentional). - Behavioral coverage for
SendTaskStatusNotificationAsync(today there's only round-trip serialization coverage — nothing verifies an actual server→client emission end-to-end, and nothing in the task wrapper auto-emits). - Make
PollTaskToCompletionAsync's stuck-detector threshold (currentlyMaxConsecutiveStuckPolls = 60) configurable onMcpClientOptions— useful for slow networks and debugging. - Once SEP-2575 server-push (
subscriptions/listen/notifications/tasks) lands, wire auto-emit from the task wrapper so the client can opt out of polling.
Thanks!
|
The CI is red, but my agent claims to know what's up:
|
Three related fixes to the task-store wrapper in McpServerImpl, all in the same catch chain around L920-970: modelcontextprotocol#2 - Failed payload shape + JsonDocument lifetime (SEP-2663 186) - Register JsonRpcErrorDetail in McpJsonUtilities source-gen context. - Replace JsonDocument.Parse(...).RootElement (leaks pooled buffer) with typed JsonRpcErrorDetail + SerializeToElement. - Emit {code, message} per spec; McpProtocolException preserves its ErrorCode + Message (documented safe to propagate); all other exceptions redact to InternalError + generic message. - Test: McpProtocolException_FromTool_StoresAsFailedWithJsonRpcErrorShape. modelcontextprotocol#3 - Orphan-on-IsTask=true - When TaskStore is configured AND CallToolWithTaskHandler returns IsTask=true, the store's pre-created task was left in Working forever. Now fails the store's task with a clear InternalError identifying the misconfiguration (use only one mechanism). - New test class: TaskStoreOrphanedTaskTests. modelcontextprotocol#4 - Dedicated InputRequiredException catch - When [McpServerTool] throws InputRequiredException under the task wrapper, it fell through to the generic catch and surfaced as a misleading Failed task. The taskId was already returned to the client synchronously, so InputRequiredResult cannot be surfaced retroactively. Now fails the task with an actionable InvalidRequest message pointing the user to CallToolWithTaskHandler. - Test: InputRequiredException_FromTool_FailsTaskWithActionableMessage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Docs: clarify IMcpTaskStore strong-consistency contract (SEP-2663 modelcontextprotocol#306) on CreateTaskAsync and document the singleton requirement under stateless HTTP; extend tasks.md custom-store requirements list to match (modelcontextprotocol#5, modelcontextprotocol#7). - Tests: add RootsTool fixture + E2E test covering server-initiated roots/list redirection through the task store (modelcontextprotocol#6); add two E2E tests for SendTaskStatusNotificationAsync covering the Working/Completed and Failed branches (modelcontextprotocol#9); add McpServerTasksNoStoreTests pinning the silent sync fallback when a client opts into tasks but no TaskStore is configured (modelcontextprotocol#8). - Rename CompletedTaskResult.TaskResult and CompletedTaskNotificationParams.TaskResult to Result for consistency with the JSON wire name. Wire format unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The stuck-in-InputRequired guard in PollTaskToCompletionAsync was hard-coded to 60 consecutive polls. Expose it as McpClientOptions .MaxConsecutiveStuckPolls (default 60, validated >= 1) so callers can tune the effective wall-clock timeout against their server's configured poll cadence (~MaxConsecutiveStuckPolls * pollIntervalMs). - Added private protected abstract plumbing on McpClient with override in McpClientImpl that surfaces _options.MaxConsecutiveStuckPolls. - PollTaskToCompletionAsync reads the option once at entry; both the threshold check and the exception message now reflect the configured value verbatim. - Documented the option, default, and timeout formula in tasks.md. - Tests: HonorsConfiguredThreshold (custom 3 surfaces "3 consecutive polls" in the McpException message), RejectsNonPositive theory (0, -1, MinValue), and DefaultsTo60. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous LongRunningTasks sample was removed in cec5d99 with the prior SEP-1686 implementation. Add a fresh self-contained sample for the SEP-2663 surface so external adopters have a runnable starting point. samples/TasksExtension/: - Program.cs wires an in-process server + client over an in-memory Pipe (no external transport required). Server registers InMemoryMcpTaskStore and a single `run-report` tool; client invokes it twice — first via CallToolAsync (auto-poll) and then via CallToolRawAsync to demonstrate manual GetTaskAsync polling. - README.md describes both paths, prints the expected console output, and points at docs/concepts/tasks/tasks.md for production guidance. - Registered in ModelContextProtocol.slnx between QuickstartWeatherServer and TestServerWithHosting. docs/concepts/tasks/tasks.md: - Updated manual-poll comment `CompletedTaskResult by deserializing TaskResult` to `deserializing its Result property` after the rename in 0b8944f. - Fixed the custom IMcpTaskStore.ResolveInputRequestsAsync signature in the implementer example from `IDictionary<string, JsonElement>` to the actual `IDictionary<string, InputResponse>`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two trivial doc-only conflicts in src/ModelContextProtocol.Core/Server/McpServer.Methods.cs from upstream PR modelcontextprotocol#1609 (Add diagnostics for messages dropped on the GET SSE stream), which added <remarks> blocks to SampleAsync and ElicitAsync about preferring RequestContext under Streamable HTTP. Resolution: accepted both upstream <remarks> blocks; kept our SampleAsync signature (ValueTask, non-async) which the prior MRTR merge (12c522f) deliberately reverted from async because SEP-2663 replaces MRTR's SendRequestWithTaskStatusTrackingAsync with our SendRequestViaTaskAsync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The server emits the Working and Completed notifications in strict order (each SendTaskStatusNotificationAsync awaits the transport write), but the client-side McpSessionHandler dispatches each incoming message via a fire-and-forget Task with a forced thread-pool yield, so user-registered notification handlers may observe them out of receipt order. Net10's thread-pool scheduling exposed this race intermittently. Give the two notifications distinct LastUpdatedAt values on the server and sort by that timestamp before asserting types/payloads, so the test asserts the round-trip without depending on client-side dispatch order. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- tasks.md: ImmutableDictionary\\2 BCL xref does not resolve under docfx (no xref service configured for System.Collections.Immutable). Replace with inline code. - stateless.md: correct namespace on IMcpTaskStore/InMemoryMcpTaskStore xrefs (ModelContextProtocol.Server.*, not ModelContextProtocol.*) and update the broken bookmark to ''tasks.md#fault-tolerant-task-implementations'' (a heading from the old MRTR-era tasks.md that this PR replaces) to point at the SEP-2663 tasks.md section ''implementing-a-custom-task-store''. Verified: dotnet docfx docs/docfx.json --warningsAsErrors true reports 0 warnings, 0 errors locally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…zed notification The test asserts strict counts for both requests and notifications observed by the incoming message filter, but ListToolsAsync only synchronizes on the request/response exchanges (initialize, tools/list). The notifications/initialized message in between is fire-and-forget on the client, and the server dispatches each incoming message via McpSessionHandler.ProcessMessageAsync as a fire-and-forget Task with a forced thread-pool yield. So the assertion can run before the notification has even reached the filter pipeline, surfacing as ''Expected: 1, Actual: 0'' for the notification count. Observed on Ubuntu/Release/net9.0 CI; reproduces under load on any target. Apply the same TaskCompletionSource pattern already used by the immediately following AddIncomingMessageFilter_Multiple_Filters_Execute_In_Order test: set a TCS inside the filter when the InitializedNotification is observed, and await it before snapshotting the counts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This PR replaces the experimental MRTR (SEP-1686) task APIs shipped in ModelContextProtocol.Core 1.3.0 with the SEP-2663 tasks extension. NuGet Package Validation (EnablePackageValidation=true, PackageValidationBaselineVersion=1.3.0) flags the resulting type and member removals as breaking changes (CP0001/CP0002/CP0011) and fails 'dotnet pack' on Release. Follow the established repo pattern (compare PR modelcontextprotocol#1368 "Add client completion notification and details", which introduced a 60-line suppressions file in the same PR that removed McpClient.Completion; that file was deleted in PR modelcontextprotocol#1621 "Bump version to 2.0.0-preview.1" once the new baseline already excluded the removed APIs). Generated via: dotnet pack -c Release -p:ApiCompatGenerateSuppressionFile=true The 320 entries (80 per TFM x 4 TFMs: net8.0, net9.0, net10.0, netstandard2.0) are entirely MRTR types and members that were marked [Experimental("MCPEXP001")] in 1.3.0 and are being replaced. A future PR that bumps PackageValidationBaselineVersion past 2.0.0-preview.1 (once published) should delete this file in the same way PR modelcontextprotocol#1621 deleted its predecessor. Verified: 'dotnet pack -c Release' completes with 0 warnings / 0 errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dbb7a20
into
modelcontextprotocol:main
jeffhandley
left a comment
There was a problem hiding this comment.
Submitting as post-merge feedback to consider.
| /// <summary> | ||
| /// Gets or sets the default time-to-live in milliseconds for new tasks, or <see langword="null"/> for unlimited. | ||
| /// </summary> | ||
| public long? DefaultTtlMs { get; set; } |
There was a problem hiding this comment.
I'm tempted to recommend the spelled-out DefaultTimeToLiveMs because DefaultTtlMs is tough to read, but I don't think my signal is strong enough to actually recommend it. Just a "worth considering for readability" suggestion instead.
|
|
||
| /// <summary> | ||
| /// Provides an in-memory implementation of <see cref="IMcpTaskStore"/> for development and testing. | ||
| /// Provides an in-memory implementation of <see cref="IMcpTaskStore"/> for development and testing scenarios. |
There was a problem hiding this comment.
Oooh, interesting. If this is only for development/testing scenarios, should we conjure up a new package for dev/test scenarios to keep this kind of API out of the core/production packages?
There was a problem hiding this comment.
I don't think it'd be worth it for this one type, and I cannot think of too many other types that would fit. Perhaps the System.IO.Stream-backed transports, StreamServerTransport and StreamClientTransport, but those aren't big either.
I see this as a little more like SignalR having an in-memory backplane by default except it require the developer using it to be even more explicit about relying on an in-memory store. I think that there are plenty of scenarios beyond dev and testing where you have a constrained number of concurrent tasks and it's fine to drop tasks when the process crashes. I see calling out development and testing scenarios as one of the main use cases though.
Brings in PR #1579 SEP-2663 Tasks (squash dbb7a20), SEP-990 Enterprise Managed Authorization (8202bcc), SEP-2243 alignment (ed19286), ttlMs renames in McpSessionHandler (711e5bb), and several quality-of-life fixes that landed between the previous merge and today. Conflict resolutions: - src/ModelContextProtocol.Core/McpJsonUtilities.cs: keep both sides' JsonSerializable additions. Our draft additions (JsonElement, Implementation, ClientCapabilities, ServerCapabilities, LoggingLevel) coexist with origin/main's IDictionary<string,object> addition. - src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs: take origin/main's renamed TaskStatusNotification value ('notifications/tasks', formerly 'notifications/tasks/status') and the updated XML docs from PR #1579. Keep all our draft additions (RelatedTaskMetaKey, SubscriptionsAcknowledgedNotification, ProtocolVersionMetaKey, ClientInfoMetaKey, ClientCapabilitiesMetaKey, LogLevelMetaKey, SubscriptionIdMetaKey). - tests/ModelContextProtocol.AspNetCore.Tests/HttpTaskIntegrationTests.cs: removed. Our pre-rebase tweak to the old SEP-1686 file is moot now that PR #1579's reimplementation deleted it; the new task tests live elsewhere. PR #1579 author addressed the reconciliation items predicted in the preview-merge analysis: '17f95f79 Fix _meta' nests tasks opt-in inside the SEP-2575 capabilities envelope (preview commit 89295fb3 no longer needed); '8b47086d Address PR feedback' fixes Failed task payload shape + JsonDocument lifetime (preview commit 8817c9fc no longer needed); '0b8944f9 Address PR feedback: docs' adds the IMcpTaskStore lifetime/ stateless docs (preview commit 072222db no longer needed). The only preview reconciliation that may still be required is gating per-request capability merge to stateful sessions only (preview commit 8b95d2ca), which is evaluated separately after this merge by re-running StatelessServerTests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR #1579's GetMetaWithTaskCapability writes a partial SEP-2575 capabilities envelope (only `extensions.io.modelcontextprotocol/tasks`) on every `tools/call`, regardless of negotiated protocol version. The server's `CreateDraftStateSyncFilter` was treating the envelope as authoritative and overwriting the session-scoped `_clientCapabilities` with the partial value - wiping out whatever the initialize handshake had captured. Most visibly, a legacy client that handshook with `Elicitation = new()` would lose elicitation support the moment it issued a tools/call, and the back-compat MRTR resolver would then fail with "Client does not support elicitation requests". Switch the per-request synchronization to a defensive merge that preserves fields the envelope leaves null and additively merges extension keys. Gate the merge behind `IsStatefulSession()` so per-request envelope state doesn't leak into `_clientCapabilities` on stateless HTTP sessions (where StatelessServerTests rely on the null invariant to surface "X is not supported in stateless mode" errors). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR #1579 (SEP-2663) replaced the SEP-1686 McpTask type with CreateTaskResult and switched its ttl/pollInterval to bare `long?` properties, so the TimeSpanMillisecondsConverter no longer has a second consumer. The shared regression suite cherry-picked from PR #1623 references the now-removed McpTask type and stops compiling. The converter's clamp-instead-of-throw branches are still fully exercised by CacheableResultTests (oversized, large-negative, +Inf, -Inf round-trips), so no coverage is lost. Drop the file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes #1573
Implement SEP-2663 Tasks Extension
Re-implements the experimental MCP tasks extension to track SEP-2663, replacing the SEP-1686 implementation removed in cec5d99. Long-running tool invocations can now be offloaded to a background task that the client polls via
tasks/get, with cooperative cancellation and multi-round-trip input requests.Experimental — gated behind
MCPEXP001.Highlights
io.modelcontextprotocol/taskskey to a request''s_meta(under the SEP-2575clientCapabilities.extensionsenvelope) to signal task support; the server must not returnCreateTaskResultotherwise (enforced inMcpServerImpl).McpServerOptions.TaskStore = new InMemoryMcpTaskStore()and every[McpServerTool]invocation is automatically wrapped, withtasks/get/tasks/update/tasks/cancelhandlers wired from the store. Explicit handlers inMcpServerOptions.Handlersoverride any slot.McpClient.CallToolAsyncinjects the extension marker, polls until terminal, dispatches input requests through the registered sampling/elicitation/roots handlers, and dedupes resolved keys across polls.CallToolRawAsyncreturns the rawResultOrCreatedTask<T>for manual lifecycle management.tasks/canceltransitions the task in the store and signals a per-taskCancellationTokenSourceso the tool''sCancellationTokenobserves the cancel. Disposal races are resolved withTryRemove.server.ElicitAsync/server.SampleAsync/server.RequestRootsAsynccalled from inside a tool scope are redirected through the store as input requests instead of direct JSON-RPC, surfaced to the client viaInputRequiredTaskResult.InputRequests, and resumed via the store''sInputResponseReceivedevent whentasks/updatearrives. Custom handlers can opt in viaMcpServer.CreateMcpTaskScope(taskId, store).Public API surface
Protocol (
ModelContextProtocol.Core/Protocol)CreateTaskResult,GetTaskResult(+Working/InputRequired/Completed/Cancelled/Failedsubtypes)UpdateTaskRequestParams/UpdateTaskResult,CancelTaskRequestParams/CancelTaskResult,GetTaskRequestParamsTaskStatusNotificationParams(+ 5 subtypes — scaffolding for SEP-2575 push notifications)ResultOrCreatedTask<T>discriminator with implicit convertersMcpTaskStatusenum (snake_case wire values)McpExtensions.TasksconstantResult.ResultTypediscriminator ("task"/"complete")Server (
ModelContextProtocol.Core/Server)IMcpTaskStore+InMemoryMcpTaskStore(immutable record snapshots, lock-free CAS)McpTaskInfo,InputResponseReceivedEventArgsMcpServerOptions.TaskStoreMcpServerHandlers.{CallToolWithTaskHandler, GetTaskHandler, UpdateTaskHandler, CancelTaskHandler}(CallToolHandler⊥CallToolWithTaskHandlermutual exclusion enforced at set time)McpServer.CreateMcpTaskScope(taskId, store)McpServer.SendTaskStatusNotificationAsync(polymorphic notification dispatch fornotifications/tasks/*)Client (
ModelContextProtocol.Core/Client)McpClient.CallToolAsync(task-aware auto-polling)McpClient.CallToolRawAsync(no auto-polling)McpClient.GetTaskAsync/UpdateTaskAsync/CancelTaskAsyncMcpClientOptions.MaxConsecutiveStuckPolls(configures the stuck-task detector threshold; default60)Safety nets baked in
CreateTaskResultwithouttasks/getwired throwsInvalidOperationExceptionat request time so misconfigured deployments fail loudly instead of producing unpollable tasks.CallToolAsyncgives up after a configurable number of consecutiveInputRequiredpolls with no new input request keys (default60, tunable viaMcpClientOptions.MaxConsecutiveStuckPolls), issues a best-efforttasks/cancel, and throwsMcpExceptionso a misbehaving server can''t trap the client in an unbounded poll loop.IsError = trueproduceCompleted(per SEP) —Failedis reserved for JSON-RPC protocol errors, with the payload shaped as a JSON-RPC error object{ code, message }.Elicit/Sample/RequestRootsskip the negotiated-capability checks when called inside a task scope, since the tasks extension itself is the negotiated capability (the client opted in via_meta).Tests
ModelContextProtocol.Testspassing on .NET 10, 364ModelContextProtocol.AspNetCore.Testspassing.McpServerTaskTests,McpTaskStoreTests,McpServerTasksNoStoreTests,InMemoryMcpTaskStoreTests,McpClientTaskMethodsTests,TaskSerializationTests,TaskCancellationIntegrationTests,TaskHandlerConfigurationValidationTests,TaskPollStuckDetectorTests,TaskStoreOrphanedTaskTests.GetTaskResultsubtype, store CAS/idempotency, terminal-state transitions,isError→Completedmapping, capability bypass, mutual exclusion ofCallToolHandler/CallToolWithTaskHandler, handler-set validation, stuck-detector cancellation flow with the configurable threshold, server→clientnotifications/tasks/*round-trip viaSendTaskStatusNotificationAsync,roots/listredirection through the task store, no-store sync fallback when the client opts in but the server has none configured, and parity tests preserved/restored from the removed SEP-1686 implementation.Sample
samples/TasksExtension/runs a server (configured withInMemoryMcpTaskStore) and a client over an in-memory pipe in a single process. Demonstrates bothCallToolAsync(auto-poll) andCallToolRawAsync(manualGetTaskAsyncloop with fullGetTaskResultsubtype handling).Documentation
docs/concepts/tasks/tasks.mdend-to-end: API examples, status/cancellation semantics, stuck-detector + configuration, custom store guidance (including strong-consistency requirements onCreateTaskAsyncper SEP-2663 §306 and singleton-under-stateless-HTTP requirements), capability bypass, known limitations.IMcpTaskStore, the three task lifecycle handlers,McpTaskExecutionContext,McpClientOptions.MaxConsecutiveStuckPolls, and theCallToolAsync(string, …)overload.Known limitations (carried forward)
SendTaskStatusNotificationAsyncis plumbed end-to-end but auto-emission from the task wrapper is not wired yet; clients still rely on polling.CreateTaskAsyncis invoked eagerly even for tools that return inline.[McpServerTool]methods can''t start sync and then escalate; useCallToolWithTaskHandlerfor that pattern.ServerCapabilities.Extensionssource-gen round-trip remains lossy for arbitraryobjectpayloads.