@@ -349,8 +349,18 @@ impl TaskManager {
349349 let future = make_future ( context) ;
350350 let inner = self . inner . clone ( ) ;
351351 let id_for_task = task_id. clone ( ) ;
352+ // SEP-2260: a task operation runs on a detached `tokio::spawn`, which
353+ // does not inherit the caller's task-local `ORIGINATING_REQUEST`
354+ // scope. Capture the originating client request id (if we are inside a
355+ // request handler, which is the normal case for `tools/call`) and
356+ // re-establish it inside the spawned task. Without this, legitimate
357+ // nested sampling/elicitation/roots requests surfaced mid-task would
358+ // be wrongly rejected as unassociated.
359+ let originating_request = crate :: service:: ORIGINATING_REQUEST
360+ . try_with ( |id| id. clone ( ) )
361+ . ok ( ) ;
352362 let handle = tokio:: spawn ( async move {
353- let result = future. await ;
363+ let result = run_task_operation ( originating_request , future) . await ;
354364 let mut inner = inner. lock ( ) . expect ( "task manager lock poisoned" ) ;
355365 if let Some ( entry) = inner. tasks . get_mut ( & id_for_task) {
356366 if entry. terminal . is_none ( ) {
@@ -538,6 +548,24 @@ fn unknown_task(task_id: &str) -> McpError {
538548 McpError :: invalid_params ( format ! ( "unknown task: {task_id}" ) , None )
539549}
540550
551+ /// Run a task operation future, re-establishing the originating request's
552+ /// SEP-2260 association scope if one was captured at spawn time.
553+ ///
554+ /// When the operation was started from within a request handler (the normal
555+ /// `tools/call` path), `originating_request` carries that request's id and the
556+ /// future runs inside an `ORIGINATING_REQUEST` scope so nested
557+ /// sampling/elicitation/roots requests are recognized as associated. When
558+ /// there is no originating request, the future runs unscoped.
559+ async fn run_task_operation (
560+ originating_request : Option < crate :: model:: RequestId > ,
561+ future : TaskFuture ,
562+ ) -> Result < CallToolResult , TaskExit > {
563+ match originating_request {
564+ Some ( id) => crate :: service:: ORIGINATING_REQUEST . scope ( id, future) . await ,
565+ None => future. await ,
566+ }
567+ }
568+
541569fn result_to_object ( result : & CallToolResult ) -> JsonObject {
542570 match serde_json:: to_value ( result) {
543571 Ok ( serde_json:: Value :: Object ( map) ) => map,
@@ -917,4 +945,77 @@ mod tests {
917945 }
918946 panic ! ( "task did not complete after input response" ) ;
919947 }
948+
949+ // SEP-2260 (GAP 2): a task operation runs on a detached `tokio::spawn`,
950+ // which does not inherit the caller's `ORIGINATING_REQUEST` task-local.
951+ // Verify the originating request's association scope is re-established
952+ // inside the spawned task so nested sampling/elicitation/roots requests
953+ // are recognized as associated.
954+ #[ tokio:: test]
955+ async fn task_operation_reestablishes_request_association_scope ( ) {
956+ use crate :: {
957+ model:: RequestId ,
958+ service:: { ORIGINATING_REQUEST , in_request_handler_scope} ,
959+ } ;
960+
961+ let manager = TaskManager :: new ( ) ;
962+ let observed = Arc :: new ( Mutex :: new ( None :: < bool > ) ) ;
963+ let observed_in_task = observed. clone ( ) ;
964+
965+ // Spawn from within a request-handler scope (as `tools/call` does).
966+ ORIGINATING_REQUEST
967+ . scope ( RequestId :: Number ( 7 ) , async {
968+ manager. spawn ( TaskOptions :: default ( ) , move |_ctx| {
969+ let observed_in_task = observed_in_task. clone ( ) ;
970+ Box :: pin ( async move {
971+ * observed_in_task. lock ( ) . unwrap ( ) = Some ( in_request_handler_scope ( ) ) ;
972+ Ok ( ok_result ( "done" ) )
973+ } )
974+ } )
975+ } )
976+ . await ;
977+
978+ for _ in 0 ..100 {
979+ tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 10 ) ) . await ;
980+ if let Some ( scoped) = * observed. lock ( ) . unwrap ( ) {
981+ assert ! (
982+ scoped,
983+ "task operation must run inside the originating request's association scope"
984+ ) ;
985+ return ;
986+ }
987+ }
988+ panic ! ( "task operation did not run" ) ;
989+ }
990+
991+ // SEP-2260 (GAP 2): a task spawned outside any request-handler scope must
992+ // not fabricate an association scope.
993+ #[ tokio:: test]
994+ async fn task_operation_without_originating_request_is_unscoped ( ) {
995+ use crate :: service:: in_request_handler_scope;
996+
997+ let manager = TaskManager :: new ( ) ;
998+ let observed = Arc :: new ( Mutex :: new ( None :: < bool > ) ) ;
999+ let observed_in_task = observed. clone ( ) ;
1000+
1001+ manager. spawn ( TaskOptions :: default ( ) , move |_ctx| {
1002+ let observed_in_task = observed_in_task. clone ( ) ;
1003+ Box :: pin ( async move {
1004+ * observed_in_task. lock ( ) . unwrap ( ) = Some ( in_request_handler_scope ( ) ) ;
1005+ Ok ( ok_result ( "done" ) )
1006+ } )
1007+ } ) ;
1008+
1009+ for _ in 0 ..100 {
1010+ tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 10 ) ) . await ;
1011+ if let Some ( scoped) = * observed. lock ( ) . unwrap ( ) {
1012+ assert ! (
1013+ !scoped,
1014+ "task operation started without an originating request must remain unscoped"
1015+ ) ;
1016+ return ;
1017+ }
1018+ }
1019+ panic ! ( "task operation did not run" ) ;
1020+ }
9201021}
0 commit comments