@@ -493,280 +493,175 @@ Tools can be configured to run asynchronously, allowing for long-running operati
493493
494494Tools 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.
495495
496- <!-- snippet-source examples/snippets/servers/async_tools .py -->
496+ <!-- snippet-source examples/snippets/servers/async_tool_basic .py -->
497497``` python
498498"""
499- FastMCP async tools example showing different invocation modes .
499+ Basic async tool example.
500500
501501cd to the `examples/snippets/clients` directory and run:
502- uv run server async_tools stdio
502+ uv run server async_tool_basic stdio
503503"""
504504
505505import asyncio
506506
507- from pydantic import BaseModel, Field
508-
509- from mcp import types
510507from mcp.server.fastmcp import Context, FastMCP
511508
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" )
561510
562511
563512@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 } " )
567516
568- # Simulate long-running work with progress updates
517+ # Simulate analysis with progress updates
569518 for i in range (5 ):
570519 await asyncio.sleep(0.5 )
571520 progress = (i + 1 ) / 5
572521 await ctx.report_progress(progress, 1.0 , f " Processing step { i + 1 } /5 " )
573522
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 "
576525
577526
578527@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 ."""
581530 if ctx:
582- # Async mode - we have context for progress reporting
531+ # Async mode with context
583532 import asyncio
584533
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 )
589537
590- # Run the async work (this is a bit of a hack for demo purposes)
591538 try :
592539 loop = asyncio.get_event_loop()
593- loop.create_task(async_work ())
540+ loop.create_task(async_processing ())
594541 except RuntimeError :
595- pass # No event loop running
542+ pass
596543
597- # Both sync and async modes return the same result
598- return f " Hybrid result: { message.upper()} "
544+ return f " Processed: { text.upper()} "
599545
600546
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+ ```
604550
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 -->
605553
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:
610555
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
616569
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" )
619571
620572
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. " )]
625576
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 )
630577
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 "
633590
634591
635592if __name__ == " __main__" :
636593 mcp.run()
637594```
638595
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 ) _
640597<!-- /snippet-source -->
641598
642599Clients using protocol version ` next ` can interact with async tools by polling operation status and retrieving results:
643600
644- <!-- snippet-source examples/snippets/clients/async_tools_client .py -->
601+ <!-- snippet-source examples/snippets/clients/async_tool_client .py -->
645602``` python
646603"""
647- Client example showing how to use async tools, including immediate result functionality .
604+ Client example for async tools.
648605
649606cd 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
653608"""
654609
655610import asyncio
656611import os
657- import sys
658612
659613from mcp import ClientSession, StdioServerParameters, types
660614from mcp.client.stdio import stdio_client
661- from mcp.shared.context import RequestContext
662615
663- # Create server parameters for stdio connection
616+ # Server parameters for async tool example
664617server_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" ],
667620 env = {" UV_INDEX" : os.environ.get(" UV_INDEX" , " " )},
668621)
669622
670623
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... " )
674627
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" })
677629
678630 if result.operation:
679631 token = result.operation.token
680- print (f " Async operation started with token: { token} " )
632+ print (f " Operation started with token: { token} " )
681633
682- # Poll for status updates
634+ # Poll for completion
683635 while True :
684636 status = await session.get_operation_status(token)
685637 print (f " Status: { status.status} " )
686638
687639 if status.status == " completed" :
688- # Get the final result
689640 final_result = await session.get_operation_result(token)
690641 for content in final_result.result.content:
691642 if isinstance (content, types.TextContent):
692- print (f " Final result : { content.text} " )
643+ print (f " Result : { content.text} " )
693644 break
694645 elif status.status == " failed" :
695646 print (f " Operation failed: { status.error} " )
696647 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 ===" )
708648
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 )
744650
745651
746652async def run ():
747- """ Run async tool demonstrations."""
748- protocol_version = " next" # Required for async tools support
749-
653+ """ Run the async tool client example."""
750654 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:
752656 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)
763658
764659
765660if __name__ == " __main__" :
766661 asyncio.run(run())
767662```
768663
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 ) _
770665<!-- /snippet-source -->
771666
772667The ` @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