Skip to content

Commit 37fb963

Browse files
committed
Add snippet for async tool with keepalive
1 parent 600982e commit 37fb963

File tree

2 files changed

+61
-4
lines changed

2 files changed

+61
-4
lines changed

examples/snippets/servers/async_tools.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,5 +135,27 @@ async def batch_operation_tool(items: list[str], ctx: Context) -> list[str]: #
135135
return results
136136

137137

138+
@mcp.tool(invocation_modes=["async"], keep_alive=1800)
139+
async def long_running_task(task_name: str, ctx: Context) -> str: # type: ignore[type-arg]
140+
"""A long-running task with custom keep_alive duration."""
141+
await ctx.info(f"Starting long-running task: {task_name}")
142+
143+
# Simulate extended processing
144+
await asyncio.sleep(2)
145+
await ctx.report_progress(0.5, 1.0, "Halfway through processing")
146+
await asyncio.sleep(2)
147+
148+
await ctx.info(f"Task '{task_name}' completed successfully")
149+
return f"Long-running task '{task_name}' finished with 30-minute keep_alive"
150+
151+
152+
@mcp.tool(invocation_modes=["async"], keep_alive=2)
153+
async def quick_expiry_task(message: str, ctx: Context) -> str: # type: ignore[type-arg]
154+
"""A task with very short keep_alive for testing expiry."""
155+
await ctx.info(f"Quick task starting: {message}")
156+
await asyncio.sleep(1)
157+
return f"Quick task completed: {message} (expires in 2 seconds)"
158+
159+
138160
if __name__ == "__main__":
139161
mcp.run()

tests/server/fastmcp/test_integration.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# pyright: reportUnknownVariableType=false
1111
# pyright: reportUnknownArgumentType=false
1212

13+
import asyncio
1314
import json
1415
import multiprocessing
1516
import socket
@@ -702,9 +703,6 @@ async def test_async_tools(server_transport: str, server_url: str) -> None:
702703
assert async_result.operation is not None
703704
token = async_result.operation.token
704705

705-
# Poll for completion
706-
import asyncio
707-
708706
while True:
709707
status = await session.get_operation_status(token)
710708
if status.status == "completed":
@@ -717,14 +715,51 @@ async def test_async_tools(server_transport: str, server_url: str) -> None:
717715
break
718716
elif status.status == "failed":
719717
pytest.fail(f"Async operation failed: {status.error}")
720-
await asyncio.sleep(0.01)
721718

722719
# Test hybrid tool (should work as sync by default)
723720
hybrid_result = await session.call_tool("hybrid_tool", {"message": "hello"})
724721
assert len(hybrid_result.content) == 1
725722
assert isinstance(hybrid_result.content[0], TextContent)
726723
assert "Hybrid result: HELLO" in hybrid_result.content[0].text
727724

725+
# Test long-running task with custom keep_alive
726+
long_task_result = await session.call_tool("long_running_task", {"task_name": "test_task"})
727+
assert long_task_result.operation is not None
728+
long_token = long_task_result.operation.token
729+
730+
while True:
731+
status = await session.get_operation_status(long_token)
732+
if status.status == "completed":
733+
final_result = await session.get_operation_result(long_token)
734+
assert not final_result.result.isError
735+
assert len(final_result.result.content) == 1
736+
content = final_result.result.content[0]
737+
assert isinstance(content, TextContent)
738+
assert "Long-running task 'test_task' finished with 30-minute keep_alive" in content.text
739+
break
740+
elif status.status == "failed":
741+
pytest.fail(f"Long-running task failed: {status.error}")
742+
743+
# Test quick expiry task (should complete then expire)
744+
quick_result = await session.call_tool("quick_expiry_task", {"message": "test_expiry"})
745+
assert quick_result.operation is not None
746+
quick_token = quick_result.operation.token
747+
748+
# Wait for completion
749+
while True:
750+
status = await session.get_operation_status(quick_token)
751+
if status.status == "completed":
752+
break
753+
elif status.status == "failed":
754+
pytest.fail(f"Quick task failed: {status.error}")
755+
756+
# Wait for expiry (2 seconds + buffer)
757+
await asyncio.sleep(3)
758+
759+
# Should now be expired
760+
with pytest.raises(Exception): # Should raise error when trying to access expired operation
761+
await session.get_operation_result(quick_token)
762+
728763

729764
# Test async tools example with legacy protocol
730765
@pytest.mark.anyio

0 commit comments

Comments
 (0)