60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
223 lines
7.7 KiB
Python
223 lines
7.7 KiB
Python
"""
|
|
Tests for Task client context validation.
|
|
|
|
Verifies that Task methods properly validate client context and that
|
|
cached results remain accessible outside context.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from fastmcp import FastMCP
|
|
from fastmcp.client import Client
|
|
|
|
|
|
@pytest.fixture
|
|
async def task_server():
|
|
"""Create a test server with background tasks."""
|
|
mcp = FastMCP("context-test-server")
|
|
|
|
@mcp.tool(task=True)
|
|
async def background_tool(value: str) -> str:
|
|
"""Tool that runs in background."""
|
|
return f"Result: {value}"
|
|
|
|
@mcp.prompt(task=True)
|
|
async def background_prompt(topic: str) -> str:
|
|
"""Prompt that runs in background."""
|
|
return f"Prompt about {topic}"
|
|
|
|
@mcp.resource("file://background.txt", task=True)
|
|
async def background_resource() -> str:
|
|
"""Resource that runs in background."""
|
|
return "Background resource content"
|
|
|
|
return mcp
|
|
|
|
|
|
async def test_task_status_outside_context_raises(task_server):
|
|
"""Calling task.status() outside client context raises error."""
|
|
task = None
|
|
async with Client(task_server) as client:
|
|
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
|
|
assert not task.returned_immediately
|
|
# Now outside context
|
|
|
|
with pytest.raises(RuntimeError, match="outside client context"):
|
|
await task.status()
|
|
|
|
|
|
async def test_task_result_outside_context_raises(task_server):
|
|
"""Calling task.result() outside context raises error."""
|
|
task = None
|
|
async with Client(task_server) as client:
|
|
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
|
|
assert not task.returned_immediately
|
|
# Now outside context
|
|
|
|
with pytest.raises(RuntimeError, match="outside client context"):
|
|
await task.result()
|
|
|
|
|
|
async def test_task_wait_outside_context_raises(task_server):
|
|
"""Calling task.wait() outside context raises error."""
|
|
task = None
|
|
async with Client(task_server) as client:
|
|
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
|
|
assert not task.returned_immediately
|
|
# Now outside context
|
|
|
|
with pytest.raises(RuntimeError, match="outside client context"):
|
|
await task.wait()
|
|
|
|
|
|
async def test_task_cancel_outside_context_raises(task_server):
|
|
"""Calling task.cancel() outside context raises error."""
|
|
task = None
|
|
async with Client(task_server) as client:
|
|
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
|
|
assert not task.returned_immediately
|
|
# Now outside context
|
|
|
|
with pytest.raises(RuntimeError, match="outside client context"):
|
|
await task.cancel()
|
|
|
|
|
|
async def test_cached_tool_task_accessible_outside_context(task_server):
|
|
"""Tool tasks with cached results work outside context."""
|
|
task = None
|
|
async with Client(task_server) as client:
|
|
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
|
|
assert not task.returned_immediately
|
|
|
|
# Get result once to cache it
|
|
result1 = await task.result()
|
|
assert result1.data == "Result: test"
|
|
# Now outside context
|
|
|
|
# Should work because result is cached
|
|
result2 = await task.result()
|
|
assert result2 is result1 # Same object
|
|
assert result2.data == "Result: test"
|
|
|
|
|
|
@pytest.mark.xfail(
|
|
reason="SDK v2 has no `task` field on GetPromptRequestParams / "
|
|
"ReadResourceRequestParams; prompt/resource task submission is not "
|
|
"wire-expressible and always graceful-degrades (sdk-feedback #3).",
|
|
strict=True,
|
|
)
|
|
async def test_cached_prompt_task_accessible_outside_context(task_server):
|
|
"""Prompt tasks with cached results work outside context."""
|
|
task = None
|
|
async with Client(task_server) as client:
|
|
task = await client.get_prompt(
|
|
"background_prompt", {"topic": "test"}, task=True
|
|
)
|
|
assert not task.returned_immediately
|
|
|
|
# Get result once to cache it
|
|
result1 = await task.result()
|
|
assert result1.description == "Prompt that runs in background."
|
|
# Now outside context
|
|
|
|
# Should work because result is cached
|
|
result2 = await task.result()
|
|
assert result2 is result1 # Same object
|
|
assert result2.description == "Prompt that runs in background."
|
|
|
|
|
|
@pytest.mark.xfail(
|
|
reason="SDK v2 has no `task` field on GetPromptRequestParams / "
|
|
"ReadResourceRequestParams; prompt/resource task submission is not "
|
|
"wire-expressible and always graceful-degrades (sdk-feedback #3).",
|
|
strict=True,
|
|
)
|
|
async def test_cached_resource_task_accessible_outside_context(task_server):
|
|
"""Resource tasks with cached results work outside context."""
|
|
task = None
|
|
async with Client(task_server) as client:
|
|
task = await client.read_resource("file://background.txt", task=True)
|
|
assert not task.returned_immediately
|
|
|
|
# Get result once to cache it
|
|
result1 = await task.result()
|
|
assert len(result1) > 0
|
|
# Now outside context
|
|
|
|
# Should work because result is cached
|
|
result2 = await task.result()
|
|
assert result2 is result1 # Same object
|
|
|
|
|
|
async def test_uncached_status_outside_context_raises(task_server):
|
|
"""Even after caching result, status() still requires client context."""
|
|
task = None
|
|
async with Client(task_server) as client:
|
|
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
|
|
assert not task.returned_immediately
|
|
|
|
# Cache the result
|
|
await task.result()
|
|
# Now outside context
|
|
|
|
# result() works (cached)
|
|
result = await task.result()
|
|
assert result.data == "Result: test"
|
|
|
|
# But status() still needs client connection
|
|
with pytest.raises(RuntimeError, match="outside client context"):
|
|
await task.status()
|
|
|
|
|
|
async def test_task_await_syntax_outside_context_raises(task_server):
|
|
"""Using await task syntax outside context raises error for background tasks."""
|
|
task = None
|
|
async with Client(task_server) as client:
|
|
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
|
|
assert not task.returned_immediately
|
|
# Now outside context
|
|
|
|
with pytest.raises(RuntimeError, match="outside client context"):
|
|
await task # Same as await task.result()
|
|
|
|
|
|
async def test_task_await_syntax_works_for_cached_results(task_server):
|
|
"""Using await task syntax works outside context when result is cached."""
|
|
task = None
|
|
async with Client(task_server) as client:
|
|
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
|
|
result1 = await task # Cache it
|
|
# Now outside context
|
|
|
|
result2 = await task # Should work (cached)
|
|
assert result2 is result1
|
|
assert result2.data == "Result: test"
|
|
|
|
|
|
async def test_multiple_result_calls_return_same_cached_object(task_server):
|
|
"""Multiple result() calls return the same cached object."""
|
|
async with Client(task_server) as client:
|
|
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
|
|
|
|
result1 = await task.result()
|
|
result2 = await task.result()
|
|
result3 = await task.result()
|
|
|
|
# Should all be the same object (cached)
|
|
assert result1 is result2
|
|
assert result2 is result3
|
|
|
|
|
|
async def test_background_task_properties_accessible_outside_context(task_server):
|
|
"""Background task properties like task_id accessible outside context."""
|
|
task = None
|
|
async with Client(task_server) as client:
|
|
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
|
|
task_id_inside = task.task_id
|
|
assert not task.returned_immediately
|
|
# Now outside context
|
|
|
|
# Properties should still be accessible (they don't need client connection)
|
|
assert task.task_id == task_id_inside
|
|
assert task.returned_immediately is False
|