Add IMcpTaskExecutor for delegating task execution to an external runtime - #1843
Open
trey-herrington wants to merge 5 commits into
Open
Add IMcpTaskExecutor for delegating task execution to an external runtime#1843trey-herrington wants to merge 5 commits into
trey-herrington wants to merge 5 commits into
Conversation
Replace the hard-coded process-local Task.Run dispatch with executor selection: McpTasksOptions.TaskExecutor, then a single IMcpTaskExecutor registered in DI, then the process-local default. StartAsync failures mark the task failed via SetFailedAsync on the existing background recording path. Behavior with no custom executor is unchanged.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new server-side extension point to delegate MCP Tasks execution to an external runtime while preserving the existing in-process default behavior and keeping task state authoritative in IMcpTaskStore.
Changes:
- Introduces
IMcpTaskExecutorandMcpTaskExecutionContextto allow task execution handoff (or local pipeline execution viaRunToolPipelineAsync). - Rewires
WithTasksbackground execution to dispatch through the executor (options override → DI → process-local default). - Adds tests and documentation covering external delegation, cancellation, scope ownership, and failure semantics.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/ModelContextProtocol.Tests/Server/McpServerTaskExecutorTests.cs | Adds coverage for custom executors, scope disposal, cancellation wiring, and start-failure behavior. |
| src/ModelContextProtocol.Extensions.Tasks/Server/ProcessLocalMcpTaskExecutor.cs | Implements the default process-local executor using the existing pipeline behavior. |
| src/ModelContextProtocol.Extensions.Tasks/Server/McpTasksOptions.cs | Adds an options-level TaskExecutor override with DI fallback semantics. |
| src/ModelContextProtocol.Extensions.Tasks/Server/McpTasksBuilderExtensions.cs | Routes task execution start through IMcpTaskExecutor and adds start-failure recording and disposal helpers. |
| src/ModelContextProtocol.Extensions.Tasks/Server/McpTaskExecutionContext.cs | Defines the context contract for executor implementations (request scope, cancellation, pipeline helper, disposal). |
| src/ModelContextProtocol.Extensions.Tasks/Server/IMcpTaskExecutor.cs | Adds the executor interface contract and semantics documentation. |
| docs/concepts/tasks/tasks.md | Documents delegating task execution and known limitations (elicitation/sampling outside session-owning process). |
Suppressed comments (1)
src/ModelContextProtocol.Extensions.Tasks/Server/McpTasksBuilderExtensions.cs:202
- The executor selection uses
_registeredExecutor, which is resolved once from the root service provider. This breaks DI lifetime expectations (scoped/transient executors won’t behave as intended) and prevents per-task scoped dependencies in executor constructors. Resolve the executor from the task’s execution scope instead (it will still return a singleton if registered as such).
var executor = _taskOptions.TaskExecutor ?? _registeredExecutor ?? ProcessLocalMcpTaskExecutor.Instance;
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ures inline Resolving IMcpTaskExecutor per-task from the execution scope instead of eagerly from the root provider gives scoped and transient registrations correct lifetimes, and resolution happens before the task record is created so a DI misconfiguration fails tools/call rather than leaving a stuck Working task. StartAsync failures are now recorded inline before the task alternate is returned, so a client's first poll observes the terminal state instead of racing it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1820.
What
Adds a stable extension point that lets
WithTasksdelegate execution to a durable system (Temporal, Orleans, Hangfire, an external queue) after the task record is created, instead of always running the tool in-process.McpTaskExecutionContextexposes the task ID and info, the matched tool request bound to a fresh execution scope, aRunToolPipelineAsynchelper that runs the normal tool pipeline locally and records the outcome in the store, a cancellation token wired totasks/cancel, andDisposeAsyncfor releasing scope-bound services when execution is handed off externally.Registration
Either on options:
or via DI:
When neither is configured, tasks execute in-process on the thread pool exactly as before, and
tasks/get,tasks/update, andtasks/cancelremain entirely onIMcpTaskStore.Semantics
StartAsyncreturns once execution is durably started (e.g. the external runtime accepted the job), mirroring the durability requirement SEP-2663 §306 places onCreateTaskAsync. It does not wait for completion; after a successfulStartAsyncthe SDK stops tracking the task and the store is the single source of truth.StartAsyncthrows, the task is marked failed viaSetFailedAsyncso the client never polls a zombie task.tasks/cancelstill callsSetCancelledAsyncand fires the context'sCancellationToken. Executors that run the pipeline locally observe it exactly as today; external executors can register on the token to propagate cancellation to their runtime.RunToolPipelineAsyncdisposes the execution scope on completion; executors that hand off externally callDisposeAsynconce they no longer needRequest(idempotent).CreateTaskAsyncand after scope/interceptor wiring, so ordering is identical whether or not a custom executor is configured.RunToolPipelineAsync, not a rawnextdelegate.Known limitation
Elicitation and sampling issued from outside the process that owns the client session cannot be routed through the task's input-request channel. External workers that need multi-round-trip input should rely on the store's
InputResponseReceivedevent or run the pipeline locally from the session-owning process. Documented in the new docs section.Commits
IMcpTaskExecutorwith an execution context for task delegation — new types only, no behavior changeWithTasksexecution throughIMcpTaskExecutor— dispatch rewire; options → DI → process-local defaultTests
New
McpServerTaskExecutorTests(9 tests) cover: context contents (task ID, working status, matched primitive, scope-bound services, tool not started until executor runs the pipeline), external completion without in-process execution, local pipeline execution recording results in the store, scope disposal after completion,StartAsyncthrowing marks the task Failed,tasks/cancelfiring the executor's token,DisposeAsyncreleasing the scope idempotently,RunToolPipelineAsyncafter dispose throwingObjectDisposedException, and late pipeline start observing cancellation.Existing task suites pass unchanged:
ModelContextProtocol.Testsnet10.0 full run 2366 passed / 2 failed (bothDockerEverythingServerTestscontainer startup timeouts, unrelated — Docker environment), AspNetCore task integration tests 18/18.Happy to adjust the interface shape based on API review feedback. The design discussion is on #1820.