fastmcp/tests/tasks/server/test_task_protocol.py
Jeremiah Lowin bb3ef39a89
Close SEP-2663 compliance gaps: -32003 on task methods, raised-error semantics, update race
- tasks/get|update|cancel now return -32003 when the client did not declare the
  tasks extension for the request (SEP-2663 MUST).
- A task tool that raises is a completed task with an is_error result, not a
  failed task; failed is reserved for protocol faults, matching a live tools/call.
- A per-task lock serializes concurrent tasks/update so two racing answers cannot
  each enqueue a next leg (double execution).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 16:59:13 -04:00

58 lines
1.9 KiB
Python

"""Protocol-level task behavior for SEP-2663 tasks.
Generic protocol behaviors driven in-process via the task helpers: a submitted
task carries a server-generated id and a TTL, and a task whose tool raises
surfaces its error rather than a result.
"""
from __future__ import annotations
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
from fastmcp_tasks import TasksExtension
from tests.tasks.task_helpers import (
run_task,
running_task_server,
submit_task,
)
def _task_server() -> FastMCP:
mcp = FastMCP("task-test-server")
mcp.add_extension(TasksExtension())
@mcp.tool(task=True)
async def simple_tool(message: str) -> str:
return f"Processed: {message}"
@mcp.tool(task=True)
async def failing_tool() -> str:
raise ToolError("This tool always fails")
return mcp
async def test_task_metadata_includes_task_id_and_ttl():
"""A submitted task carries a server-generated id and a positive TTL."""
mcp = _task_server()
async with running_task_server(mcp):
created = await submit_task(mcp, "simple_tool", {"message": "test"})
assert isinstance(created.task_id, str)
assert created.task_id
assert created.ttl_ms is not None and created.ttl_ms > 0
async def test_raised_tool_error_completes_with_is_error():
"""A task whose tool raises completes with an is_error result (SEP-2663).
`failed` is reserved for protocol faults; a raised tool error is the same
`isError` result a live tools/call returns.
"""
mcp = _task_server()
async with running_task_server(mcp):
final = await run_task(mcp, "failing_tool", {})
assert final.status == "completed"
assert final.error is None
assert final.result is not None
assert final.result["isError"] is True
assert "This tool always fails" in final.result["content"][0]["text"]