fastmcp/tests/server/tasks/test_task_protocol.py
Chris Guidry 66aaf420c9
[2.14] SEP-1686 tasks (#2378)
* Implement MCP background tasks (SEP-1686) using Docket

Adds support for background task execution via the MCP task protocol,
powered by Docket for task queue management.

- Tools, resources, and prompts can be marked with `task=True` to run async
- Progress dependency for tracking task progress
- CurrentDocket and CurrentWorker dependencies for advanced use cases
- Client API with `.call_tool(..., task=True)` returns task handles
- Task status notifications via subscriptions
- CLI worker command for distributed task processing

Configuration via environment:
- FASTMCP_ENABLE_DOCKET=true
- FASTMCP_ENABLE_TASKS=true
- FASTMCP_DOCKET_URL=redis://... (or memory:// for single-process)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix tasks example import (TaskStatusResponse → GetTaskResult)

The example was using a non-existent TaskStatusResponse type.
Updated to use mcp.types.GetTaskResult which is what the
on_status_change callback actually receives.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix env var name in Docket error messages

The error messages referenced FASTMCP_EXPERIMENTAL_ENABLE_DOCKET but the
actual setting is FASTMCP_ENABLE_DOCKET.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove deprecated code re-added from pre-#2329 branch

- Remove ExtendedEnvSettingsSource (FASTMCP_SERVER_ prefix support)
- Remove dependencies parameter from FastMCP.__init__

* Replace fakeredis git pin with PyPI release

* Remove redundant fakeredis dev dep (pulled via pydocket)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-12-04 20:10:35 -05:00

83 lines
2.5 KiB
Python

"""
Tests for SEP-1686 protocol-level task handling.
Generic protocol tests that use tools as test fixtures.
Tests metadata, notifications, and error handling at the protocol level.
"""
import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
@pytest.fixture
async def task_enabled_server():
"""Create a FastMCP server with task-enabled tools."""
mcp = FastMCP("task-test-server")
@mcp.tool(task=True)
async def simple_tool(message: str) -> str:
"""A simple tool for testing."""
return f"Processed: {message}"
@mcp.tool(task=True)
async def failing_tool() -> str:
"""A tool that always fails."""
raise ValueError("This tool always fails")
assert mcp._support_tasks_by_default
return mcp
async def test_task_metadata_includes_task_id_and_ttl(task_enabled_server):
"""Task metadata properly includes server-generated taskId and ttl."""
async with Client(task_enabled_server) as client:
# Submit with specific ttl (server generates task ID)
task = await client.call_tool(
"simple_tool",
{"message": "test"},
task=True,
ttl=30000,
)
assert task
assert not task.returned_immediately
# Server should have generated a task ID
assert task.task_id is not None
assert isinstance(task.task_id, str)
async def test_task_notification_sent_after_submission(task_enabled_server):
"""Server sends notifications/tasks/created after task submission."""
@task_enabled_server.tool(task=True)
async def background_tool(message: str) -> str:
return f"Processed: {message}"
async with Client(task_enabled_server) as client:
task = await client.call_tool("background_tool", {"message": "test"}, task=True)
assert task
assert not task.returned_immediately
# Verify we can query the task
status = await task.status()
assert status.taskId == task.task_id
async def test_failed_task_stores_error(task_enabled_server):
"""Failed tasks store the error in results."""
@task_enabled_server.tool(task=True)
async def failing_task_tool() -> str:
raise ValueError("This tool always fails")
async with Client(task_enabled_server) as client:
task = await client.call_tool("failing_task_tool", task=True)
assert task
assert not task.returned_immediately
# Wait for task to fail
status = await task.wait(state="failed", timeout=2.0)
assert status.status == "failed"