11using ModelContextProtocol . Protocol ;
22using ModelContextProtocol . Server ;
33using Microsoft . Extensions . DependencyInjection ;
4- using System . Collections . Concurrent ;
54using System . Runtime . InteropServices ;
65using System . Text . Json ;
76using System . Text . Json . Nodes ;
@@ -179,17 +178,25 @@ public async Task CallToolAsync_AsyncTool_FailedTask_ThrowsMcpException()
179178 await using var client = await CreateMcpClientForServer ( ) ;
180179 var ct = TestContext . Current . CancellationToken ;
181180
182- _ = Task . Run ( async ( ) =>
181+ var failedTask = new TaskCompletionSource < bool > ( ) ;
182+
183+ // Run failure task once the task from the tool call is created
184+ _taskStore . OnTaskCreated += taskId =>
183185 {
184- await Task . Delay ( 100 , ct ) ;
185- var taskId = _taskStore . GetAllTaskIds ( ) . Single ( ) ;
186- _taskStore . FailTask ( taskId , JsonElement . Parse ( """{"code":-32000,"message":"something went wrong"}""" ) ) ;
187- } , ct ) ;
186+ _ = Task . Run ( async ( ) =>
187+ {
188+ await Task . Delay ( 100 , ct ) ;
189+ _taskStore . FailTask ( taskId , JsonElement . Parse ( """{"code":-32000,"message":"something went wrong"}""" ) ) ;
190+ failedTask . SetResult ( true ) ;
191+ } , ct ) ;
192+ } ;
188193
189194 await Assert . ThrowsAsync < McpException > ( async ( ) =>
190195 await client . CallToolAsync (
191196 new CallToolRequestParams { Name = "async-tool" } ,
192197 ct ) ) ;
198+
199+ Assert . True ( await failedTask . Task ) ;
193200 }
194201
195202 [ Fact ]
@@ -198,17 +205,25 @@ public async Task CallToolAsync_AsyncTool_CancelledTask_ThrowsOperationCancelled
198205 await using var client = await CreateMcpClientForServer ( ) ;
199206 var ct = TestContext . Current . CancellationToken ;
200207
201- _ = Task . Run ( async ( ) =>
208+ var cancelledTask = new TaskCompletionSource < bool > ( ) ;
209+
210+ // Run cancellation task once the task from the tool call is created
211+ _taskStore . OnTaskCreated += taskId =>
202212 {
203- await Task . Delay ( 100 , ct ) ;
204- var taskId = _taskStore . GetAllTaskIds ( ) . Single ( ) ;
205- _taskStore . CancelTask ( taskId ) ;
206- } , ct ) ;
213+ Task . Run ( async ( ) =>
214+ {
215+ await Task . Delay ( 100 , ct ) ;
216+ _taskStore . CancelTask ( taskId ) ;
217+ cancelledTask . SetResult ( true ) ;
218+ } , ct ) ;
219+ } ;
207220
208221 await Assert . ThrowsAsync < OperationCanceledException > ( async ( ) =>
209222 await client . CallToolAsync (
210223 new CallToolRequestParams { Name = "async-tool" } ,
211224 ct ) ) ;
225+
226+ Assert . True ( await cancelledTask . Task ) ;
212227 }
213228
214229 [ Fact ]
@@ -538,106 +553,135 @@ public async Task CallToolHandler_CanBeSetToNull_ThenOtherCanBeSet()
538553 /// </summary>
539554 private sealed class InMemoryTaskStore
540555 {
541- private readonly ConcurrentDictionary < string , TaskEntry > _tasks = new ( ) ;
556+ private readonly Dictionary < string , TaskEntry > _tasks = new ( ) ;
557+
558+ internal Action < string > ? OnTaskCreated ;
542559
543560 public string CreateTask ( McpTaskStatus initialStatus = McpTaskStatus . Working )
544561 {
545562 var taskId = Guid . NewGuid ( ) . ToString ( "N" ) ;
546- _tasks [ taskId ] = new TaskEntry
563+ lock ( _tasks )
547564 {
548- Status = initialStatus ,
549- CreatedAt = DateTimeOffset . UtcNow ,
550- LastUpdatedAt = DateTimeOffset . UtcNow ,
551- } ;
565+ _tasks [ taskId ] = new TaskEntry
566+ {
567+ Status = initialStatus ,
568+ CreatedAt = DateTimeOffset . UtcNow ,
569+ LastUpdatedAt = DateTimeOffset . UtcNow ,
570+ } ;
571+ }
572+
573+ OnTaskCreated ? . Invoke ( taskId ) ;
574+
552575 return taskId ;
553576 }
554577
555- public IEnumerable < string > GetAllTaskIds ( ) => _tasks . Keys ;
556-
557- public GetTaskResult GetTask ( string taskId )
578+ public IEnumerable < string > GetAllTaskIds ( )
558579 {
559- if ( ! _tasks . TryGetValue ( taskId , out var entry ) )
580+ lock ( _tasks )
560581 {
561- throw new McpException ( $ "Unknown task: ' { taskId } '" ) ;
582+ return _tasks . Keys . ToArray ( ) ;
562583 }
584+ }
563585
564- return entry . Status switch
586+ public GetTaskResult GetTask ( string taskId )
587+ {
588+ lock ( _tasks )
565589 {
566- McpTaskStatus . Working => new WorkingTaskResult
567- {
568- TaskId = taskId ,
569- CreatedAt = entry . CreatedAt ,
570- LastUpdatedAt = entry . LastUpdatedAt ,
571- PollIntervalMs = 50 ,
572- } ,
573- McpTaskStatus . Completed => new CompletedTaskResult
590+ if ( ! _tasks . TryGetValue ( taskId , out var entry ) )
574591 {
575- TaskId = taskId ,
576- CreatedAt = entry . CreatedAt ,
577- LastUpdatedAt = entry . LastUpdatedAt ,
578- Result = JsonSerializer . SerializeToElement ( entry . Result , McpJsonUtilities . DefaultOptions ) ,
579- } ,
580- McpTaskStatus . Failed => new FailedTaskResult
581- {
582- TaskId = taskId ,
583- CreatedAt = entry . CreatedAt ,
584- LastUpdatedAt = entry . LastUpdatedAt ,
585- Error = entry . Error ! . Value ,
586- } ,
587- McpTaskStatus . Cancelled => new CancelledTaskResult
588- {
589- TaskId = taskId ,
590- CreatedAt = entry . CreatedAt ,
591- LastUpdatedAt = entry . LastUpdatedAt ,
592- } ,
593- McpTaskStatus . InputRequired => new InputRequiredTaskResult
592+ throw new McpException ( $ "Unknown task: '{ taskId } '") ;
593+ }
594+
595+ return entry . Status switch
594596 {
595- TaskId = taskId ,
596- CreatedAt = entry . CreatedAt ,
597- LastUpdatedAt = entry . LastUpdatedAt ,
598- InputRequests = entry . InputRequests ?? new Dictionary < string , InputRequest > ( ) ,
599- } ,
600- _ => throw new InvalidOperationException ( $ "Unexpected status: { entry . Status } ")
601- } ;
597+ McpTaskStatus . Working => new WorkingTaskResult
598+ {
599+ TaskId = taskId ,
600+ CreatedAt = entry . CreatedAt ,
601+ LastUpdatedAt = entry . LastUpdatedAt ,
602+ PollIntervalMs = 50 ,
603+ } ,
604+ McpTaskStatus . Completed => new CompletedTaskResult
605+ {
606+ TaskId = taskId ,
607+ CreatedAt = entry . CreatedAt ,
608+ LastUpdatedAt = entry . LastUpdatedAt ,
609+ Result = JsonSerializer . SerializeToElement ( entry . Result , McpJsonUtilities . DefaultOptions ) ,
610+ } ,
611+ McpTaskStatus . Failed => new FailedTaskResult
612+ {
613+ TaskId = taskId ,
614+ CreatedAt = entry . CreatedAt ,
615+ LastUpdatedAt = entry . LastUpdatedAt ,
616+ Error = entry . Error ! . Value ,
617+ } ,
618+ McpTaskStatus . Cancelled => new CancelledTaskResult
619+ {
620+ TaskId = taskId ,
621+ CreatedAt = entry . CreatedAt ,
622+ LastUpdatedAt = entry . LastUpdatedAt ,
623+ } ,
624+ McpTaskStatus . InputRequired => new InputRequiredTaskResult
625+ {
626+ TaskId = taskId ,
627+ CreatedAt = entry . CreatedAt ,
628+ LastUpdatedAt = entry . LastUpdatedAt ,
629+ InputRequests = entry . InputRequests ?? new Dictionary < string , InputRequest > ( ) ,
630+ } ,
631+ _ => throw new InvalidOperationException ( $ "Unexpected status: { entry . Status } ")
632+ } ;
633+ }
602634 }
603635
604636 public void CompleteTask ( string taskId , CallToolResult result )
605637 {
606- if ( _tasks . TryGetValue ( taskId , out var entry ) )
638+ lock ( _tasks )
607639 {
608- entry . Status = McpTaskStatus . Completed ;
609- entry . Result = result ;
610- entry . LastUpdatedAt = DateTimeOffset . UtcNow ;
640+ if ( _tasks . TryGetValue ( taskId , out var entry ) )
641+ {
642+ entry . Result = result ;
643+ entry . LastUpdatedAt = DateTimeOffset . UtcNow ;
644+ entry . Status = McpTaskStatus . Completed ;
645+ }
611646 }
612647 }
613648
614649 public void FailTask ( string taskId , JsonElement error )
615650 {
616- if ( _tasks . TryGetValue ( taskId , out var entry ) )
651+ lock ( _tasks )
617652 {
618- entry . Status = McpTaskStatus . Failed ;
619- entry . Error = error ;
620- entry . LastUpdatedAt = DateTimeOffset . UtcNow ;
653+ if ( _tasks . TryGetValue ( taskId , out var entry ) )
654+ {
655+ entry . Error = error ;
656+ entry . LastUpdatedAt = DateTimeOffset . UtcNow ;
657+ entry . Status = McpTaskStatus . Failed ;
658+ }
621659 }
622660 }
623661
624662 public void CancelTask ( string taskId )
625663 {
626- if ( _tasks . TryGetValue ( taskId , out var entry ) )
664+ lock ( _tasks )
627665 {
628- entry . Status = McpTaskStatus . Cancelled ;
629- entry . LastUpdatedAt = DateTimeOffset . UtcNow ;
666+ if ( _tasks . TryGetValue ( taskId , out var entry ) )
667+ {
668+ entry . LastUpdatedAt = DateTimeOffset . UtcNow ;
669+ entry . Status = McpTaskStatus . Cancelled ;
670+ }
630671 }
631672 }
632673
633674 public void ProvideInput ( string taskId , IDictionary < string , InputResponse > inputResponses )
634675 {
635- if ( _tasks . TryGetValue ( taskId , out var entry ) )
676+ lock ( _tasks )
636677 {
637- entry . InputResponses = inputResponses ;
638- // Transition back to working after receiving input
639- entry . Status = McpTaskStatus . Working ;
640- entry . LastUpdatedAt = DateTimeOffset . UtcNow ;
678+ if ( _tasks . TryGetValue ( taskId , out var entry ) )
679+ {
680+ entry . InputResponses = inputResponses ;
681+ entry . LastUpdatedAt = DateTimeOffset . UtcNow ;
682+ // Transition back to working after receiving input
683+ entry . Status = McpTaskStatus . Working ;
684+ }
641685 }
642686 }
643687
0 commit comments