@@ -275,6 +275,72 @@ public async Task CallToolAsTask_Succeeds_WhenToolHasRequiredTaskSupport()
275275 Assert . NotNull ( result . Task . TaskId ) ;
276276 }
277277
278+ [ Fact ]
279+ public async Task CallToolAsTask_WithRequiredTaskSupport_CanResolveScopedServicesFromDI ( )
280+ {
281+ // Regression test for https://github.com/modelcontextprotocol/csharp-sdk/issues/1430:
282+ // ExecuteToolAsTaskAsync fires Task.Run and returns immediately, so the request-scoped
283+ // IServiceProvider owned by InvokeHandlerAsync is disposed before the background task
284+ // calls tool.InvokeAsync. The fix creates a fresh scope inside the Task.Run body so the
285+ // tool can resolve DI services without hitting ObjectDisposedException.
286+ var taskStore = new InMemoryMcpTaskStore ( ) ;
287+ string ? capturedValue = null ;
288+
289+ await using var fixture = new ServerClientFixture ( LoggerFactory , configureServer : ( services , builder ) =>
290+ {
291+ services . AddSingleton < IMcpTaskStore > ( taskStore ) ;
292+ services . Configure < McpServerOptions > ( options => options . TaskStore = taskStore ) ;
293+
294+ // Register a scoped service; resolving it through a disposed scope was the bug.
295+ services . AddScoped < ITaskToolDiService , TaskToolDiService > ( ) ;
296+
297+ // Register the tool via the factory pattern so that Services = sp is threaded
298+ // through, enabling DI parameter binding at tool-creation time.
299+ builder . Services . AddSingleton < McpServerTool > ( sp => McpServerTool . Create (
300+ async ( ITaskToolDiService svc , CancellationToken ct ) =>
301+ {
302+ await Task . Delay ( 10 , ct ) ;
303+ capturedValue = svc . GetValue ( ) ;
304+ return capturedValue ;
305+ } ,
306+ new McpServerToolCreateOptions
307+ {
308+ Name = "di-required-task-tool" ,
309+ Services = sp ,
310+ Execution = new ToolExecution { TaskSupport = ToolTaskSupport . Required }
311+ } ) ) ;
312+ } ) ;
313+
314+ await using var client = await fixture . CreateClientAsync ( TestContext . Current . CancellationToken ) ;
315+
316+ var result = await client . CallToolAsync (
317+ new CallToolRequestParams
318+ {
319+ Name = "di-required-task-tool" ,
320+ Task = new McpTaskMetadata ( )
321+ } ,
322+ TestContext . Current . CancellationToken ) ;
323+
324+ Assert . NotNull ( result . Task ) ;
325+ string taskId = result . Task . TaskId ;
326+
327+ // Poll until the background task reaches a terminal state.
328+ McpTask taskStatus ;
329+ int attempts = 0 ;
330+ do
331+ {
332+ await Task . Delay ( 50 , TestContext . Current . CancellationToken ) ;
333+ taskStatus = await client . GetTaskAsync ( taskId , cancellationToken : TestContext . Current . CancellationToken ) ;
334+ attempts ++ ;
335+ }
336+ while ( taskStatus . Status == McpTaskStatus . Working && attempts < 50 ) ;
337+
338+ // Without the fix, the background task would fail with ObjectDisposedException when
339+ // resolving ITaskToolDiService, causing the task to reach McpTaskStatus.Failed.
340+ Assert . Equal ( McpTaskStatus . Completed , taskStatus . Status ) ;
341+ Assert . Equal ( "hello-from-di" , capturedValue ) ;
342+ }
343+
278344 [ Fact ]
279345 public async Task CallToolAsTaskAsync_WithProgress_CreatesTaskSuccessfully ( )
280346 {
@@ -857,6 +923,16 @@ public async Task NormalRequest_Succeeds_WhenTasksNotSupported()
857923
858924 #endregion
859925
926+ private interface ITaskToolDiService
927+ {
928+ string GetValue ( ) ;
929+ }
930+
931+ private sealed class TaskToolDiService : ITaskToolDiService
932+ {
933+ public string GetValue ( ) => "hello-from-di" ;
934+ }
935+
860936 /// <summary>
861937 /// Helper fixture for creating server-client pairs with custom configuration.
862938 /// </summary>
0 commit comments