@@ -67,6 +67,7 @@ async def main():
6767
6868from __future__ import annotations as _annotations
6969
70+ import asyncio
7071import contextvars
7172import json
7273import logging
@@ -465,46 +466,55 @@ async def handler(req: types.CallToolRequest):
465466 except jsonschema .ValidationError as e :
466467 return self ._make_error_result (f"Input validation error: { e .message } " )
467468
468- # tool call
469- results = await func (tool_name , arguments )
469+ # Check for async execution
470+ if tool and self .async_operations and self ._should_execute_async (tool ):
471+ # Create async operation
472+ session_id = f"session_{ id (self .request_context .session )} "
473+ operation = self .async_operations .create_operation (
474+ tool_name = tool_name ,
475+ arguments = arguments ,
476+ session_id = session_id ,
477+ )
478+ logger .debug (f"Created async operation with token: { operation .token } " )
470479
471- # output normalization
472- unstructured_content : UnstructuredContent
473- maybe_structured_content : StructuredContent | None
474- if isinstance (results , tuple ) and len (results ) == 2 :
475- # tool returned both structured and unstructured content
476- unstructured_content , maybe_structured_content = cast (CombinationContent , results )
477- elif isinstance (results , dict ):
478- # tool returned structured content only
479- maybe_structured_content = cast (StructuredContent , results )
480- unstructured_content = [types .TextContent (type = "text" , text = json .dumps (results , indent = 2 ))]
481- elif hasattr (results , "__iter__" ):
482- # tool returned unstructured content only
483- unstructured_content = cast (UnstructuredContent , results )
484- maybe_structured_content = None
485- else :
486- return self ._make_error_result (f"Unexpected return type from tool: { type (results ).__name__ } " )
487-
488- # output validation
489- if tool and tool .outputSchema is not None :
490- if maybe_structured_content is None :
491- return self ._make_error_result (
492- "Output validation error: outputSchema defined but no structured output returned"
493- )
494- else :
480+ # Start async execution in background
481+ async def execute_async ():
495482 try :
496- jsonschema .validate (instance = maybe_structured_content , schema = tool .outputSchema )
497- except jsonschema .ValidationError as e :
498- return self ._make_error_result (f"Output validation error: { e .message } " )
499-
500- # result
501- return types .ServerResult (
502- types .CallToolResult (
503- content = list (unstructured_content ),
504- structuredContent = maybe_structured_content ,
505- isError = False ,
483+ logger .debug (f"Starting async execution of { tool_name } " )
484+ results = await func (tool_name , arguments )
485+ logger .debug (f"Async execution completed for { tool_name } " )
486+
487+ # Process results using shared logic
488+ result = self ._process_tool_result (results , tool )
489+ self .async_operations .complete_operation (operation .token , result )
490+ logger .debug (f"Completed async operation { operation .token } " )
491+ except Exception as e :
492+ logger .exception (f"Async execution failed for { tool_name } " )
493+ self .async_operations .fail_operation (operation .token , str (e ))
494+
495+ asyncio .create_task (execute_async ())
496+
497+ # Return operation result immediately
498+ logger .info (f"Returning async operation result for { tool_name } " )
499+ return types .ServerResult (
500+ types .CallToolResult (
501+ content = [],
502+ operation = types .AsyncResultProperties (
503+ token = operation .token ,
504+ keepAlive = 3600 ,
505+ ),
506+ )
506507 )
507- )
508+
509+ # tool call
510+ results = await func (tool_name , arguments )
511+
512+ # Process results using shared logic
513+ try :
514+ result = self ._process_tool_result (results , tool )
515+ return types .ServerResult (result )
516+ except ValueError as e :
517+ return self ._make_error_result (str (e ))
508518 except Exception as e :
509519 return self ._make_error_result (str (e ))
510520
@@ -513,6 +523,61 @@ async def handler(req: types.CallToolRequest):
513523
514524 return decorator
515525
526+ def _process_tool_result (
527+ self , results : UnstructuredContent | StructuredContent | CombinationContent , tool : types .Tool | None = None
528+ ) -> types .CallToolResult :
529+ """Process tool results and create CallToolResult with validation."""
530+ # output normalization
531+ unstructured_content : UnstructuredContent
532+ maybe_structured_content : StructuredContent | None
533+ if isinstance (results , tuple ) and len (results ) == 2 :
534+ # tool returned both structured and unstructured content
535+ unstructured_content , maybe_structured_content = cast (CombinationContent , results )
536+ elif isinstance (results , dict ):
537+ # tool returned structured content only
538+ maybe_structured_content = cast (StructuredContent , results )
539+ unstructured_content = [types .TextContent (type = "text" , text = json .dumps (results , indent = 2 ))]
540+ elif hasattr (results , "__iter__" ):
541+ # tool returned unstructured content only
542+ unstructured_content = cast (UnstructuredContent , results )
543+ maybe_structured_content = None
544+ else :
545+ raise ValueError (f"Unexpected return type from tool: { type (results ).__name__ } " )
546+
547+ # output validation
548+ if tool and tool .outputSchema is not None :
549+ if maybe_structured_content is None :
550+ raise ValueError ("Output validation error: outputSchema defined but no structured output returned" )
551+ else :
552+ try :
553+ jsonschema .validate (instance = maybe_structured_content , schema = tool .outputSchema )
554+ except jsonschema .ValidationError as e :
555+ raise ValueError (f"Output validation error: { e .message } " )
556+
557+ # result
558+ return types .CallToolResult (
559+ content = list (unstructured_content ),
560+ structuredContent = maybe_structured_content ,
561+ isError = False ,
562+ )
563+
564+ def _should_execute_async (self , tool : types .Tool ) -> bool :
565+ """Check if a tool should be executed asynchronously."""
566+ # Check if client supports async tools (protocol version "next")
567+ try :
568+ if self .request_context and self .request_context .session .client_params :
569+ client_version = str (self .request_context .session .client_params .protocolVersion )
570+ if client_version != "next" :
571+ return False
572+ else :
573+ return False
574+ except (AttributeError , ValueError ):
575+ return False
576+
577+ # Check if tool is async-only
578+ invocation_mode = getattr (tool , "invocationMode" , None )
579+ return invocation_mode == "async"
580+
516581 def progress_notification (self ):
517582 def decorator (
518583 func : Callable [[str | int , float , float | None , str | None ], Awaitable [None ]],
@@ -783,9 +848,9 @@ async def _handle_request(
783848 # Track async operations for cancellation
784849 if isinstance (req , types .CallToolRequest ):
785850 result = response .root
786- if isinstance (result , types .CallToolResult ) and result .operation_result is not None :
851+ if isinstance (result , types .CallToolResult ) and result .operation is not None :
787852 # This is an async operation, track the request ID to token mapping
788- operation_token = result .operation_result .token
853+ operation_token = result .operation .token
789854 self ._request_to_operation [message .request_id ] = operation_token
790855 logger .debug (f"Tracking async operation { operation_token } for request { message .request_id } " )
791856
0 commit comments