Skip to content

Commit b0d3f30

Browse files
committed
Split up async tool snippets to improve README readability
1 parent 97be6dd commit b0d3f30

13 files changed

+1141
-947
lines changed

README.md

Lines changed: 73 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -493,280 +493,175 @@ Tools can be configured to run asynchronously, allowing for long-running operati
493493

494494
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.
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
501501
cd 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

505505
import asyncio
506506

507-
from pydantic import BaseModel, Field
508-
509-
from mcp import types
510507
from 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

635592
if __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

642599
Clients 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
649606
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
653608
"""
654609

655610
import asyncio
656611
import os
657-
import sys
658612

659613
from mcp import ClientSession, StdioServerParameters, types
660614
from 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
664617
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"],
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"\nAsync 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("\nFinal 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

746652
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."""
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

765660
if __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

772667
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

Comments
 (0)