@@ -493,280 +493,175 @@ Tools can be configured to run asynchronously, allowing for long-running operati
493
493
494
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
495
496
- <!-- snippet-source examples/snippets/servers/async_tools .py -->
496
+ <!-- snippet-source examples/snippets/servers/async_tool_basic .py -->
497
497
``` python
498
498
"""
499
- FastMCP async tools example showing different invocation modes .
499
+ Basic async tool example.
500
500
501
501
cd to the `examples/snippets/clients` directory and run:
502
- uv run server async_tools stdio
502
+ uv run server async_tool_basic stdio
503
503
"""
504
504
505
505
import asyncio
506
506
507
- from pydantic import BaseModel, Field
508
-
509
- from mcp import types
510
507
from mcp.server.fastmcp import Context, FastMCP
511
508
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 } "
509
+ mcp = FastMCP(" Async Tool Basic" )
561
510
562
511
563
512
@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... " )
513
+ async def analyze_data ( dataset : str , ctx : Context) -> str : # type: ignore [ type -arg ]
514
+ """ Analyze a dataset asynchronously with progress updates ."""
515
+ await ctx.info(f " Starting analysis of { dataset } " )
567
516
568
- # Simulate long-running work with progress updates
517
+ # Simulate analysis with progress updates
569
518
for i in range (5 ):
570
519
await asyncio.sleep(0.5 )
571
520
progress = (i + 1 ) / 5
572
521
await ctx.report_progress(progress, 1.0 , f " Processing step { i + 1 } /5 " )
573
522
574
- await ctx.info(" Analysis complete! " )
575
- return f " Async analysis result for: { data } "
523
+ await ctx.info(" Analysis complete" )
524
+ return f " Analysis results for { dataset } : 95% accuracy achieved "
576
525
577
526
578
527
@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."""
528
+ def process_text ( text : str , ctx : Context | None = None ) -> str : # type: ignore [ type -arg ]
529
+ """ Process text in sync or async mode ."""
581
530
if ctx:
582
- # Async mode - we have context for progress reporting
531
+ # Async mode with context
583
532
import asyncio
584
533
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" )
534
+ async def async_processing ():
535
+ await ctx.info(f " Processing text asynchronously: { text[:20 ]} ... " )
536
+ await asyncio.sleep(0.3 )
589
537
590
- # Run the async work (this is a bit of a hack for demo purposes)
591
538
try :
592
539
loop = asyncio.get_event_loop()
593
- loop.create_task(async_work ())
540
+ loop.create_task(async_processing ())
594
541
except RuntimeError :
595
- pass # No event loop running
542
+ pass
596
543
597
- # Both sync and async modes return the same result
598
- return f " Hybrid result: { message.upper()} "
544
+ return f " Processed: { text.upper()} "
599
545
600
546
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. " )]
547
+ if __name__ == " __main__ " :
548
+ mcp.run()
549
+ ```
604
550
551
+ _ Full example: [ examples/snippets/servers/async_tool_basic.py] ( https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/async_tool_basic.py ) _
552
+ <!-- /snippet-source -->
605
553
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 " )
554
+ Tools can also provide immediate feedback while continuing to execute asynchronously:
610
555
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 " )
556
+ <!-- snippet-source examples/snippets/servers/async_tool_immediate.py -->
557
+ ``` python
558
+ """
559
+ Async tool with immediate result example.
560
+
561
+ cd to the `examples/snippets/clients` directory and run:
562
+ uv run server async_tool_immediate stdio
563
+ """
564
+
565
+ import asyncio
566
+
567
+ from mcp import types
568
+ from mcp.server.fastmcp import Context, FastMCP
616
569
617
- await ctx.info(f " Analysis ' { operation} ' completed successfully! " )
618
- return f " Analysis ' { operation} ' completed successfully with detailed results! "
570
+ mcp = FastMCP(" Async Tool Immediate" )
619
571
620
572
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} " )
573
+ async def provide_immediate_feedback (operation : str ) -> list[types.ContentBlock]:
574
+ """ Provide immediate feedback while async operation starts."""
575
+ return [types.TextContent(type = " text" , text = f " Starting { operation} operation. This will take a moment. " )]
625
576
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
577
631
- await ctx.info(f " Task ' { task_name} ' completed successfully " )
632
- return f " Long-running task ' { task_name} ' finished with 30-minute keep_alive "
578
+ @mcp.tool (invocation_modes = [" async" ], immediate_result = provide_immediate_feedback)
579
+ async def long_analysis (operation : str , ctx : Context) -> str : # type: ignore [ type -arg ]
580
+ """ Perform long-running analysis with immediate user feedback."""
581
+ await ctx.info(f " Beginning { operation} analysis " )
582
+
583
+ # Simulate long-running work
584
+ for i in range (4 ):
585
+ await asyncio.sleep(1 )
586
+ progress = (i + 1 ) / 4
587
+ await ctx.report_progress(progress, 1.0 , f " Analysis step { i + 1 } /4 " )
588
+
589
+ return f " Analysis ' { operation} ' completed with detailed results "
633
590
634
591
635
592
if __name__ == " __main__" :
636
593
mcp.run()
637
594
```
638
595
639
- _ Full example: [ examples/snippets/servers/async_tools .py] ( https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/async_tools .py ) _
596
+ _ Full example: [ examples/snippets/servers/async_tool_immediate .py] ( https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/async_tool_immediate .py ) _
640
597
<!-- /snippet-source -->
641
598
642
599
Clients using protocol version ` next ` can interact with async tools by polling operation status and retrieving results:
643
600
644
- <!-- snippet-source examples/snippets/clients/async_tools_client .py -->
601
+ <!-- snippet-source examples/snippets/clients/async_tool_client .py -->
645
602
``` python
646
603
"""
647
- Client example showing how to use async tools, including immediate result functionality .
604
+ Client example for async tools.
648
605
649
606
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
607
+ uv run async-tool-client
653
608
"""
654
609
655
610
import asyncio
656
611
import os
657
- import sys
658
612
659
613
from mcp import ClientSession, StdioServerParameters, types
660
614
from mcp.client.stdio import stdio_client
661
- from mcp.shared.context import RequestContext
662
615
663
- # Create server parameters for stdio connection
616
+ # Server parameters for async tool example
664
617
server_params = StdioServerParameters(
665
- command = " uv" , # Using uv to run the server
666
- args = [" run" , " server" , " async_tools " , " stdio" ],
618
+ command = " uv" ,
619
+ args = [" run" , " server" , " async_tool_basic " , " stdio" ],
667
620
env = {" UV_INDEX" : os.environ.get(" UV_INDEX" , " " )},
668
621
)
669
622
670
623
671
- async def demonstrate_async_tool (session : ClientSession):
672
- """ Demonstrate calling an async-only tool."""
673
- print (" \n === Asynchronous Tool Demo === " )
624
+ async def call_async_tool (session : ClientSession):
625
+ """ Demonstrate calling an async tool."""
626
+ print (" Calling async tool... " )
674
627
675
- # Call the async tool
676
- result = await session.call_tool(" async_only_tool" , arguments = {" data" : " sample dataset" })
628
+ result = await session.call_tool(" analyze_data" , arguments = {" dataset" : " customer_data.csv" })
677
629
678
630
if result.operation:
679
631
token = result.operation.token
680
- print (f " Async operation started with token: { token} " )
632
+ print (f " Operation started with token: { token} " )
681
633
682
- # Poll for status updates
634
+ # Poll for completion
683
635
while True :
684
636
status = await session.get_operation_status(token)
685
637
print (f " Status: { status.status} " )
686
638
687
639
if status.status == " completed" :
688
- # Get the final result
689
640
final_result = await session.get_operation_result(token)
690
641
for content in final_result.result.content:
691
642
if isinstance (content, types.TextContent):
692
- print (f " Final result : { content.text} " )
643
+ print (f " Result : { content.text} " )
693
644
break
694
645
elif status.status == " failed" :
695
646
print (f " Operation failed: { status.error} " )
696
647
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
648
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 )
649
+ await asyncio.sleep(0.5 )
744
650
745
651
746
652
async def run ():
747
- """ Run async tool demonstrations."""
748
- protocol_version = " next" # Required for async tools support
749
-
653
+ """ Run the async tool client example."""
750
654
async with stdio_client(server_params) as (read, write):
751
- async with ClientSession(read, write, protocol_version = protocol_version ) as session:
655
+ async with ClientSession(read, write, protocol_version = " next " ) as session:
752
656
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)
657
+ await call_async_tool(session)
763
658
764
659
765
660
if __name__ == " __main__" :
766
661
asyncio.run(run())
767
662
```
768
663
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 ) _
664
+ _ Full example: [ examples/snippets/clients/async_tool_client .py] ( https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/async_tool_client .py ) _
770
665
<!-- /snippet-source -->
771
666
772
667
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).
0 commit comments