@@ -487,6 +487,290 @@ def get_temperature(city: str) -> float:
487
487
_ Full example: [ examples/snippets/servers/structured_output.py] ( https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/structured_output.py ) _
488
488
<!-- /snippet-source -->
489
489
490
+ #### Async Tools
491
+
492
+ Tools can be configured to run asynchronously, allowing for long-running operations that execute in the background while clients poll for status and results. Async tools currently require protocol version ` next ` and support operation tokens for tracking execution state.
493
+
494
+ Tools can specify their invocation mode: ` sync ` (default), ` async ` , or ` ["sync", "async"] ` for hybrid tools that support both patterns. Async tools can provide immediate feedback while continuing to execute, and support configurable keep-alive duration for result availability.
495
+
496
+ <!-- snippet-source examples/snippets/servers/async_tools.py -->
497
+ ``` python
498
+ """
499
+ FastMCP async tools example showing different invocation modes.
500
+
501
+ cd to the `examples/snippets/clients` directory and run:
502
+ uv run server async_tools stdio
503
+ """
504
+
505
+ import asyncio
506
+
507
+ from pydantic import BaseModel, Field
508
+
509
+ from mcp import types
510
+ from mcp.server.fastmcp import Context, FastMCP
511
+
512
+ # Create an MCP server with async operations support
513
+ mcp = FastMCP(" Async Tools Demo" )
514
+
515
+
516
+ class UserPreferences (BaseModel ):
517
+ """ Schema for collecting user preferences."""
518
+
519
+ continue_processing: bool = Field(description = " Should we continue with the operation?" )
520
+ priority_level: str = Field(
521
+ default = " normal" ,
522
+ description = " Priority level: low, normal, high" ,
523
+ )
524
+
525
+
526
+ @mcp.tool (invocation_modes = [" async" ])
527
+ async def async_elicitation_tool (operation : str , ctx : Context) -> str : # type: ignore [ type -arg ]
528
+ """ An async tool that uses elicitation to get user input."""
529
+ await ctx.info(f " Starting operation: { operation} " )
530
+
531
+ # Simulate some initial processing
532
+ await asyncio.sleep(0.5 )
533
+ await ctx.report_progress(0.3 , 1.0 , " Initial processing complete" )
534
+
535
+ # Ask user for preferences
536
+ result = await ctx.elicit(
537
+ message = f " Operation ' { operation} ' requires user input. How should we proceed? " ,
538
+ schema = UserPreferences,
539
+ )
540
+
541
+ if result.action == " accept" and result.data:
542
+ if result.data.continue_processing:
543
+ await ctx.info(f " Continuing with { result.data.priority_level} priority " )
544
+ # Simulate processing based on user choice
545
+ processing_time = {" low" : 0.5 , " normal" : 1.0 , " high" : 1.5 }.get(result.data.priority_level, 1.0 )
546
+ await asyncio.sleep(processing_time)
547
+ await ctx.report_progress(1.0 , 1.0 , " Operation complete" )
548
+ return f " Operation ' { operation} ' completed successfully with { result.data.priority_level} priority "
549
+ else :
550
+ await ctx.warning(" User chose not to continue" )
551
+ return f " Operation ' { operation} ' cancelled by user "
552
+ else :
553
+ await ctx.error(" User declined or cancelled the operation" )
554
+ return f " Operation ' { operation} ' aborted "
555
+
556
+
557
+ @mcp.tool ()
558
+ def sync_tool (x : int ) -> str :
559
+ """ An implicitly-synchronous tool."""
560
+ return f " Sync result: { x * 2 } "
561
+
562
+
563
+ @mcp.tool (invocation_modes = [" async" ])
564
+ async def async_only_tool (data : str , ctx : Context) -> str : # type: ignore [ type -arg ]
565
+ """ An async-only tool that takes time to complete."""
566
+ await ctx.info(" Starting long-running analysis..." )
567
+
568
+ # Simulate long-running work with progress updates
569
+ for i in range (5 ):
570
+ await asyncio.sleep(0.5 )
571
+ progress = (i + 1 ) / 5
572
+ await ctx.report_progress(progress, 1.0 , f " Processing step { i + 1 } /5 " )
573
+
574
+ await ctx.info(" Analysis complete!" )
575
+ return f " Async analysis result for: { data} "
576
+
577
+
578
+ @mcp.tool (invocation_modes = [" sync" , " async" ])
579
+ def hybrid_tool (message : str , ctx : Context | None = None ) -> str : # type: ignore [ type -arg ]
580
+ """ A hybrid tool that works both sync and async."""
581
+ if ctx:
582
+ # Async mode - we have context for progress reporting
583
+ import asyncio
584
+
585
+ async def async_work ():
586
+ await ctx.info(f " Processing ' { message} ' asynchronously... " )
587
+ await asyncio.sleep(0.5 ) # Simulate some work
588
+ await ctx.debug(" Async processing complete" )
589
+
590
+ # Run the async work (this is a bit of a hack for demo purposes)
591
+ try :
592
+ loop = asyncio.get_event_loop()
593
+ loop.create_task(async_work())
594
+ except RuntimeError :
595
+ pass # No event loop running
596
+
597
+ # Both sync and async modes return the same result
598
+ return f " Hybrid result: { message.upper()} "
599
+
600
+
601
+ async def immediate_feedback (operation : str ) -> list[types.ContentBlock]:
602
+ """ Provide immediate feedback for long-running operations."""
603
+ return [types.TextContent(type = " text" , text = f " 🚀 Starting { operation} ... This may take a moment. " )]
604
+
605
+
606
+ @mcp.tool (invocation_modes = [" async" ], immediate_result = immediate_feedback)
607
+ async def long_running_analysis (operation : str , ctx : Context) -> str : # type: ignore [ type -arg ]
608
+ """ Perform analysis with immediate user feedback."""
609
+ await ctx.info(f " Beginning { operation} analysis " )
610
+
611
+ # Simulate long-running work with progress updates
612
+ for i in range (5 ):
613
+ await asyncio.sleep(1 )
614
+ progress = (i + 1 ) / 5
615
+ await ctx.report_progress(progress, 1.0 , f " Step { i + 1 } /5 complete " )
616
+
617
+ await ctx.info(f " Analysis ' { operation} ' completed successfully! " )
618
+ return f " Analysis ' { operation} ' completed successfully with detailed results! "
619
+
620
+
621
+ @mcp.tool (invocation_modes = [" async" ], keep_alive = 1800 )
622
+ async def long_running_task (task_name : str , ctx : Context) -> str : # type: ignore [ type -arg ]
623
+ """ A long-running task with custom keep_alive duration."""
624
+ await ctx.info(f " Starting long-running task: { task_name} " )
625
+
626
+ # Simulate extended processing
627
+ await asyncio.sleep(2 )
628
+ await ctx.report_progress(0.5 , 1.0 , " Halfway through processing" )
629
+ await asyncio.sleep(2 )
630
+
631
+ await ctx.info(f " Task ' { task_name} ' completed successfully " )
632
+ return f " Long-running task ' { task_name} ' finished with 30-minute keep_alive "
633
+
634
+
635
+ if __name__ == " __main__" :
636
+ mcp.run()
637
+ ```
638
+
639
+ _ Full example: [ examples/snippets/servers/async_tools.py] ( https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/async_tools.py ) _
640
+ <!-- /snippet-source -->
641
+
642
+ Clients using protocol version ` next ` can interact with async tools by polling operation status and retrieving results:
643
+
644
+ <!-- snippet-source examples/snippets/clients/async_tools_client.py -->
645
+ ``` python
646
+ """
647
+ Client example showing how to use async tools, including immediate result functionality.
648
+
649
+ cd to the `examples/snippets` directory and run:
650
+ uv run async-tools-client
651
+ uv run async-tools-client --protocol=latest # backwards compatible mode
652
+ uv run async-tools-client --protocol=next # async tools mode
653
+ """
654
+
655
+ import asyncio
656
+ import os
657
+ import sys
658
+
659
+ from mcp import ClientSession, StdioServerParameters, types
660
+ from mcp.client.stdio import stdio_client
661
+ from mcp.shared.context import RequestContext
662
+
663
+ # Create server parameters for stdio connection
664
+ server_params = StdioServerParameters(
665
+ command = " uv" , # Using uv to run the server
666
+ args = [" run" , " server" , " async_tools" , " stdio" ],
667
+ env = {" UV_INDEX" : os.environ.get(" UV_INDEX" , " " )},
668
+ )
669
+
670
+
671
+ async def demonstrate_async_tool (session : ClientSession):
672
+ """ Demonstrate calling an async-only tool."""
673
+ print (" \n === Asynchronous Tool Demo ===" )
674
+
675
+ # Call the async tool
676
+ result = await session.call_tool(" async_only_tool" , arguments = {" data" : " sample dataset" })
677
+
678
+ if result.operation:
679
+ token = result.operation.token
680
+ print (f " Async operation started with token: { token} " )
681
+
682
+ # Poll for status updates
683
+ while True :
684
+ status = await session.get_operation_status(token)
685
+ print (f " Status: { status.status} " )
686
+
687
+ if status.status == " completed" :
688
+ # Get the final result
689
+ final_result = await session.get_operation_result(token)
690
+ for content in final_result.result.content:
691
+ if isinstance (content, types.TextContent):
692
+ print (f " Final result: { content.text} " )
693
+ break
694
+ elif status.status == " failed" :
695
+ print (f " Operation failed: { status.error} " )
696
+ break
697
+ elif status.status in (" canceled" , " unknown" ):
698
+ print (f " Operation ended with status: { status.status} " )
699
+ break
700
+
701
+ # Wait before polling again
702
+ await asyncio.sleep(1 )
703
+
704
+
705
+ async def test_immediate_result_tool (session : ClientSession):
706
+ """ Test calling async tool with immediate result functionality."""
707
+ print (" \n === Immediate Result Tool Demo ===" )
708
+
709
+ # Call the async tool with immediate_result functionality
710
+ result = await session.call_tool(" long_running_analysis" , arguments = {" operation" : " data_processing" })
711
+
712
+ # Display immediate feedback (should be available immediately)
713
+ print (" Immediate response received:" )
714
+ if result.content:
715
+ for content in result.content:
716
+ if isinstance (content, types.TextContent):
717
+ print (f " 📋 { content.text} " )
718
+
719
+ # Check if there's an async operation to poll
720
+ if result.operation:
721
+ token = result.operation.token
722
+ print (f " \n Async operation started with token: { token} " )
723
+ print (" Polling for final results..." )
724
+
725
+ # Poll for status updates and final result
726
+ while True :
727
+ status = await session.get_operation_status(token)
728
+ print (f " Status: { status.status} " )
729
+
730
+ if status.status == " completed" :
731
+ # Get the final result
732
+ final_result = await session.get_operation_result(token)
733
+ print (" \n Final result received:" )
734
+ for content in final_result.result.content:
735
+ if isinstance (content, types.TextContent):
736
+ print (f " ✅ { content.text} " )
737
+ break
738
+ elif status.status == " failed" :
739
+ print (f " ❌ Operation failed: { status.error} " )
740
+ break
741
+
742
+ # Wait before polling again
743
+ await asyncio.sleep(1 )
744
+
745
+
746
+ async def run ():
747
+ """ Run async tool demonstrations."""
748
+ protocol_version = " next" # Required for async tools support
749
+
750
+ async with stdio_client(server_params) as (read, write):
751
+ async with ClientSession(read, write, protocol_version = protocol_version) as session:
752
+ await session.initialize()
753
+
754
+ # List available tools to see invocation modes
755
+ tools = await session.list_tools()
756
+ print (" Available tools:" )
757
+ for tool in tools.tools:
758
+ invocation_mode = getattr (tool, " invocationMode" , " sync" )
759
+ print (f " - { tool.name} : { tool.description} (mode: { invocation_mode} ) " )
760
+
761
+ await demonstrate_async_tool(session)
762
+ await test_immediate_result_tool(session)
763
+
764
+
765
+ if __name__ == " __main__" :
766
+ asyncio.run(run())
767
+ ```
768
+
769
+ _ Full example: [ examples/snippets/clients/async_tools_client.py] ( https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/async_tools_client.py ) _
770
+ <!-- /snippet-source -->
771
+
772
+ The ` @mcp.tool() ` decorator accepts ` invocation_modes ` to specify supported execution patterns, ` immediate_result ` to provide instant feedback for async tools, and ` keep_alive ` to set how long operation results remain available (default: 300 seconds).
773
+
490
774
### Prompts
491
775
492
776
Prompts are reusable templates that help LLMs interact with your server effectively:
0 commit comments