mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Fix ty diagnostics in task tests, scope ty exclusion to client-task files
This commit is contained in:
parent
5fa2883670
commit
bc22e517fd
18 changed files with 84 additions and 24 deletions
|
|
@ -45,9 +45,7 @@ __all__ = [
|
|||
#: the tasks extension in for the request.
|
||||
MISSING_REQUIRED_CLIENT_CAPABILITY = -32003
|
||||
|
||||
TaskStatus = Literal[
|
||||
"working", "input_required", "completed", "failed", "cancelled"
|
||||
]
|
||||
TaskStatus = Literal["working", "input_required", "completed", "failed", "cancelled"]
|
||||
|
||||
|
||||
class _TaskFields(BaseModel):
|
||||
|
|
@ -69,7 +67,9 @@ class _TaskFields(BaseModel):
|
|||
created_at: str = Field(serialization_alias="createdAt")
|
||||
last_updated_at: str = Field(serialization_alias="lastUpdatedAt")
|
||||
ttl_ms: float | None = Field(serialization_alias="ttlMs")
|
||||
status_message: str | None = Field(default=None, serialization_alias="statusMessage")
|
||||
status_message: str | None = Field(
|
||||
default=None, serialization_alias="statusMessage"
|
||||
)
|
||||
poll_interval_ms: float | None = Field(
|
||||
default=None, serialization_alias="pollIntervalMs"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -155,10 +155,9 @@ exclude = [
|
|||
"examples/providers/sqlite", # needs aiosqlite
|
||||
"examples/memory.py", # needs asyncpg, numpy, pydantic_ai, pgvector
|
||||
"examples/get_file.py", # needs aiohttp
|
||||
# The moved task tests pass at runtime but carry ty diagnostics (mostly
|
||||
# None-narrowing on optional result fields); a follow-up commit fixes them
|
||||
# and removes this exclusion.
|
||||
"tests/tasks",
|
||||
# Skipped pending client task support; rewritten in the client-task follow-up.
|
||||
"tests/tasks/client/test_task_context_validation.py",
|
||||
"tests/tasks/client/test_task_result_caching.py",
|
||||
]
|
||||
|
||||
[tool.ty.environment]
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ async def test_concurrent_background_tasks_with_context():
|
|||
assert len(finals) == 4
|
||||
for final in finals:
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"]["result"].startswith("bg:")
|
||||
|
||||
|
||||
|
|
@ -134,6 +135,7 @@ async def test_concurrent_background_tasks_with_progress():
|
|||
assert len(finals) == 4
|
||||
for final in finals:
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"]["result"].startswith("bg:")
|
||||
|
||||
|
||||
|
|
@ -202,6 +204,7 @@ async def test_sync_context_functions_work_in_background_without_deps():
|
|||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"has_headers": "False"}
|
||||
|
||||
|
||||
|
|
@ -225,4 +228,5 @@ async def test_sync_context_functions_work_in_background_with_context():
|
|||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"]["is_background"] == "True"
|
||||
|
|
|
|||
|
|
@ -356,6 +356,7 @@ class TestBackgroundTaskIntegration:
|
|||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": "done"}
|
||||
|
||||
async def test_context_wiring_in_background_task(self):
|
||||
|
|
@ -382,6 +383,7 @@ class TestBackgroundTaskIntegration:
|
|||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {
|
||||
"task_id_set": True,
|
||||
"is_background": True,
|
||||
|
|
@ -406,6 +408,7 @@ class TestBackgroundTaskIntegration:
|
|||
parked = await wait_for_task(
|
||||
mcp, created.task_id, target_states=frozenset({"input_required"})
|
||||
)
|
||||
assert parked.input_requests is not None
|
||||
key = next(iter(parked.input_requests))
|
||||
await update_task(
|
||||
mcp,
|
||||
|
|
@ -415,6 +418,7 @@ class TestBackgroundTaskIntegration:
|
|||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": "Hello, Bob!"}
|
||||
|
||||
async def test_elicit_decline_flow(self):
|
||||
|
|
@ -436,11 +440,13 @@ class TestBackgroundTaskIntegration:
|
|||
parked = await wait_for_task(
|
||||
mcp, created.task_id, target_states=frozenset({"input_required"})
|
||||
)
|
||||
assert parked.input_requests is not None
|
||||
key = next(iter(parked.input_requests))
|
||||
await update_task(mcp, created.task_id, {key: {"action": "decline"}})
|
||||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": "User declined"}
|
||||
|
||||
async def test_elicit_with_pydantic_model(self):
|
||||
|
|
@ -466,6 +472,7 @@ class TestBackgroundTaskIntegration:
|
|||
parked = await wait_for_task(
|
||||
mcp, created.task_id, target_states=frozenset({"input_required"})
|
||||
)
|
||||
assert parked.input_requests is not None
|
||||
key = next(iter(parked.input_requests))
|
||||
await update_task(
|
||||
mcp,
|
||||
|
|
@ -475,6 +482,7 @@ class TestBackgroundTaskIntegration:
|
|||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": "Alice is 30"}
|
||||
|
||||
|
||||
|
|
@ -511,6 +519,7 @@ class TestAccessTokenInBackgroundTasks:
|
|||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {
|
||||
"result": "roundtrip-jwt|test-client"
|
||||
}
|
||||
|
|
@ -530,6 +539,7 @@ class TestAccessTokenInBackgroundTasks:
|
|||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": "no-token"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ async def test_custom_tool_background_execution(custom_tool_server):
|
|||
final = await run_task(custom_tool_server, "custom_tool", {})
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert "Custom tool executed" in final.result["content"][0]["text"]
|
||||
|
||||
|
||||
|
|
@ -101,6 +102,7 @@ async def test_custom_tool_with_arguments(custom_tool_server):
|
|||
final = await run_task(custom_tool_server, "custom_logic", {"duration": 1})
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert "Completed after 1 units" in final.result["content"][0]["text"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
from contextlib import AsyncExitStack
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
from fastmcp_tasks.models import (
|
||||
|
|
@ -19,6 +20,7 @@ from fastmcp_tasks.models import (
|
|||
CreateTaskResult,
|
||||
)
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.session import ServerSession
|
||||
from mcp.shared.exceptions import MCPError
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
|
@ -140,6 +142,7 @@ async def test_required_tool_tasks_when_opted_in():
|
|||
created = await submit_task(mcp, "must_task", {"n": 10})
|
||||
final = await wait_for_task(mcp, created.task_id)
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"]["result"] == 11
|
||||
|
||||
|
||||
|
|
@ -197,6 +200,7 @@ async def test_task_arguments_are_coerced_like_sync_path():
|
|||
async with running_task_server(mcp):
|
||||
# "6" coerces to int 6 exactly as the synchronous path would.
|
||||
final = await run_task(mcp, "square", {"n": "6"})
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"]["result"] == 36
|
||||
|
||||
|
||||
|
|
@ -308,7 +312,7 @@ async def test_legacy_era_opt_in_is_ignored():
|
|||
mcp = _tasks_server()
|
||||
async with running_task_server(mcp):
|
||||
srctx = ServerRequestContext(
|
||||
session=SimpleNamespace(),
|
||||
session=cast(ServerSession, SimpleNamespace()),
|
||||
lifespan_context={},
|
||||
protocol_version="2025-06-18",
|
||||
method="tools/call",
|
||||
|
|
@ -324,7 +328,7 @@ async def test_legacy_era_required_tool_raises_missing_capability():
|
|||
mcp = _tasks_server()
|
||||
async with running_task_server(mcp):
|
||||
srctx = ServerRequestContext(
|
||||
session=SimpleNamespace(),
|
||||
session=cast(ServerSession, SimpleNamespace()),
|
||||
lifespan_context={},
|
||||
protocol_version="2025-06-18",
|
||||
method="tools/call",
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ async def test_progress_in_background_task():
|
|||
created = await submit_task(mcp, "test_task", {})
|
||||
final = await wait_for_task(mcp, created.task_id)
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": "done"}
|
||||
|
||||
|
||||
|
|
@ -97,6 +98,7 @@ async def test_progress_status_message_in_background_task():
|
|||
release.set()
|
||||
final = await wait_for_task(mcp, created.task_id)
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": "done"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ async def test_task_with_custom_tool_name():
|
|||
async with running_task_server(mcp):
|
||||
final = await run_task(mcp, "custom-tool-name")
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {
|
||||
"result": "result from custom-named tool"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ async def test_snapshot_restored_before_user_code_runs():
|
|||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": True}
|
||||
|
||||
|
||||
|
|
@ -75,6 +76,7 @@ async def test_get_access_token_in_bg_task_without_context_dep():
|
|||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": "jwt-3897"}
|
||||
|
||||
|
||||
|
|
@ -99,6 +101,7 @@ async def test_restore_failure_is_nonfatal():
|
|||
final = await wait_for_task(mcp, created.task_id)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": False}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ class TestToolExecutionMetadata:
|
|||
return "ok"
|
||||
|
||||
tool = await mcp.get_tool("my_tool")
|
||||
assert tool is not None
|
||||
execution = tool.to_mcp_tool().execution
|
||||
assert isinstance(execution, ToolExecution)
|
||||
assert execution.task_support == "optional"
|
||||
|
|
@ -194,6 +195,7 @@ class TestToolExecutionMetadata:
|
|||
return "ok"
|
||||
|
||||
tool = await mcp.get_tool("my_tool")
|
||||
assert tool is not None
|
||||
execution = tool.to_mcp_tool().execution
|
||||
assert isinstance(execution, ToolExecution)
|
||||
assert execution.task_support == "required"
|
||||
|
|
@ -208,6 +210,7 @@ class TestToolExecutionMetadata:
|
|||
return "ok"
|
||||
|
||||
tool = await mcp.get_tool("my_tool")
|
||||
assert tool is not None
|
||||
assert tool.to_mcp_tool().execution is None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ async def test_background_tool_receives_docket_dependency(dependency_server):
|
|||
final = await run_task(dependency_server, "tool_with_docket_dependency", {})
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": "Docket: True"}
|
||||
assert len(dependency_server._injected_values) == 1
|
||||
dep_type, dep_value = dependency_server._injected_values[0]
|
||||
|
|
@ -88,6 +89,7 @@ async def test_background_tool_receives_server_dependency(dependency_server):
|
|||
final = await run_task(dependency_server, "tool_with_server_dependency", {})
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {
|
||||
"result": f"Server: {dependency_server.name}"
|
||||
}
|
||||
|
|
@ -107,6 +109,7 @@ async def test_background_tool_receives_custom_depends(dependency_server):
|
|||
)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": 50} # 5 * 10
|
||||
assert len(dependency_server._injected_values) == 1
|
||||
dep_type, dep_value = dependency_server._injected_values[0]
|
||||
|
|
@ -124,6 +127,7 @@ async def test_background_tool_with_multiple_dependencies(dependency_server):
|
|||
)
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {
|
||||
"result": f"test on {dependency_server.name}"
|
||||
}
|
||||
|
|
@ -180,6 +184,7 @@ async def test_dependency_context_managers_cleaned_up_in_background():
|
|||
final = await run_task(mcp, "use_connection", {"name": "test"})
|
||||
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": "Used: connection"}
|
||||
assert cleanup_called == ["enter", "exit"]
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ async def _drive(server: FastMCP, name: str, answers: list[dict[str, Any]]) -> s
|
|||
await update_task(server, created.task_id, {key: answer})
|
||||
final = await wait_for_task(server, created.task_id)
|
||||
assert final.status == "completed", final.error
|
||||
assert final.result is not None
|
||||
return final.result["content"][0]["text"]
|
||||
|
||||
|
||||
|
|
@ -214,4 +215,5 @@ async def test_unanswered_input_times_out_to_cancel(monkeypatch):
|
|||
# Never answer; the worker's bounded wait resolves to cancel.
|
||||
final = await wait_for_task(mcp, created.task_id, timeout=10.0)
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["content"][0]["text"] == "Cancelled as expected"
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ async def test_tasks_get_returns_status_and_inlined_result():
|
|||
|
||||
final = await wait_for_task(mcp, created.task_id)
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": 42}
|
||||
assert final.result is not None
|
||||
assert final.result["isError"] is False
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ Two architectural notes vs. SEP-1686:
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import cast
|
||||
|
||||
import mcp_types as mt
|
||||
import pytest
|
||||
|
|
@ -31,7 +32,7 @@ from mcp_types import ToolExecution
|
|||
from fastmcp import Context, FastMCP
|
||||
from fastmcp.server.dependencies import CurrentFastMCP
|
||||
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
|
||||
from fastmcp.server.providers.proxy import ProxyTool
|
||||
from fastmcp.server.providers.proxy import ClientFactoryT, ProxyTool
|
||||
from fastmcp.tools.base import ToolResult
|
||||
from fastmcp.utilities.tasks import TaskConfig
|
||||
from fastmcp_tasks import TasksExtension
|
||||
|
|
@ -90,6 +91,7 @@ class TestMountedToolTasks:
|
|||
assert created.status == "working"
|
||||
final = await wait_for_task(parent_server, created.task_id)
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"]["result"] == 72
|
||||
|
||||
async def test_mounted_and_parent_tasks_both_work(self, parent_server):
|
||||
|
|
@ -102,7 +104,9 @@ class TestMountedToolTasks:
|
|||
)
|
||||
parent_final = await wait_for_task(parent_server, parent_created.task_id)
|
||||
child_final = await wait_for_task(parent_server, child_created.task_id)
|
||||
assert parent_final.result is not None
|
||||
assert parent_final.result["structuredContent"]["result"] == 50
|
||||
assert child_final.result is not None
|
||||
assert child_final.result["structuredContent"]["result"] == 6
|
||||
|
||||
async def test_sync_only_mounted_tool_runs_synchronously(self, parent_server):
|
||||
|
|
@ -129,6 +133,7 @@ class TestMountedToolTasksNoPrefix:
|
|||
parent,
|
||||
(await submit_task(parent, "multiply", {"a": 5, "b": 6})).task_id,
|
||||
)
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"]["result"] == 30
|
||||
|
||||
|
||||
|
|
@ -137,7 +142,7 @@ class TestMountedTaskDependencies:
|
|||
child = FastMCP("dep-child")
|
||||
|
||||
@child.tool(task=True)
|
||||
async def tool_with_docket(docket: CurrentDocket = CurrentDocket()) -> str: # type: ignore[assignment,valid-type]
|
||||
async def tool_with_docket(docket: Docket = CurrentDocket()) -> str:
|
||||
return f"docket available: {docket is not None}"
|
||||
|
||||
parent = FastMCP("dep-parent")
|
||||
|
|
@ -149,6 +154,7 @@ class TestMountedTaskDependencies:
|
|||
parent,
|
||||
(await submit_task(parent, "child_tool_with_docket", {})).task_id,
|
||||
)
|
||||
assert final.result is not None
|
||||
assert "docket available: True" in final.result["content"][0]["text"]
|
||||
|
||||
|
||||
|
|
@ -157,7 +163,7 @@ class TestMountedTaskServerContext:
|
|||
child = FastMCP("child")
|
||||
|
||||
@child.tool(task=True)
|
||||
async def whoami(server: CurrentFastMCP = CurrentFastMCP()) -> str: # type: ignore[assignment,valid-type]
|
||||
async def whoami(server: FastMCP = CurrentFastMCP()) -> str:
|
||||
return f"server name: {server.name}"
|
||||
|
||||
parent = FastMCP("parent")
|
||||
|
|
@ -168,6 +174,7 @@ class TestMountedTaskServerContext:
|
|||
final = await wait_for_task(
|
||||
parent, (await submit_task(parent, "child_whoami", {})).task_id
|
||||
)
|
||||
assert final.result is not None
|
||||
assert "server name: child" in final.result["content"][0]["text"]
|
||||
|
||||
async def test_context_fastmcp_resolves_to_child_server(self):
|
||||
|
|
@ -185,6 +192,7 @@ class TestMountedTaskServerContext:
|
|||
final = await wait_for_task(
|
||||
parent, (await submit_task(parent, "child_whoami_ctx", {})).task_id
|
||||
)
|
||||
assert final.result is not None
|
||||
assert "context server: child" in final.result["content"][0]["text"]
|
||||
|
||||
|
||||
|
|
@ -217,7 +225,9 @@ class TestMultipleMounts:
|
|||
await submit_task(parent, "math2_subtract", {"a": 10, "b": 5})
|
||||
).task_id,
|
||||
)
|
||||
assert r1.result is not None
|
||||
assert r1.result["structuredContent"]["result"] == 15
|
||||
assert r2.result is not None
|
||||
assert r2.result["structuredContent"]["result"] == 5
|
||||
|
||||
async def test_same_function_names_do_not_collide(self):
|
||||
|
|
@ -246,7 +256,9 @@ class TestMultipleMounts:
|
|||
parent,
|
||||
(await submit_task(parent, "c2_process", {"value": 10})).task_id,
|
||||
)
|
||||
assert r1.result is not None
|
||||
assert r1.result["structuredContent"]["result"] == 20
|
||||
assert r2.result is not None
|
||||
assert r2.result["structuredContent"]["result"] == 30
|
||||
|
||||
async def test_nested_mount_prefix_accumulation(self):
|
||||
|
|
@ -267,6 +279,7 @@ class TestMultipleMounts:
|
|||
parent,
|
||||
(await submit_task(parent, "child_gc_deep_tool", {})).task_id,
|
||||
)
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"]["result"] == "deep"
|
||||
|
||||
|
||||
|
|
@ -286,6 +299,8 @@ class TestMountedTaskMetadata:
|
|||
|
||||
child_mcp = child_tool.to_mcp_tool(name=child_tool.name)
|
||||
parent_mcp = parent_tool.to_mcp_tool(name=parent_tool.name)
|
||||
assert child_mcp.execution is not None
|
||||
assert parent_mcp.execution is not None
|
||||
assert child_mcp.execution.task_support == "optional"
|
||||
assert parent_mcp.execution.task_support == "optional"
|
||||
|
||||
|
|
@ -296,7 +311,7 @@ class TestMountedTaskMetadata:
|
|||
input_schema={"type": "object", "properties": {}},
|
||||
execution=ToolExecution(task_support="optional"),
|
||||
)
|
||||
proxy = ProxyTool.from_mcp_tool(lambda: None, mcp_tool) # type: ignore[arg-type]
|
||||
proxy = ProxyTool.from_mcp_tool(cast(ClientFactoryT, lambda: None), mcp_tool)
|
||||
result = proxy.to_mcp_tool(name=proxy.name)
|
||||
assert result.execution is not None
|
||||
assert result.execution.task_support == "optional"
|
||||
|
|
@ -339,6 +354,7 @@ class TestMountedTaskConfigModes:
|
|||
await submit_task(parent_with_modes, "child_optional_tool", {})
|
||||
).task_id,
|
||||
)
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"]["result"] == "optional result"
|
||||
|
||||
async def test_required_mode_with_task_through_mount(self, parent_with_modes):
|
||||
|
|
@ -349,6 +365,7 @@ class TestMountedTaskConfigModes:
|
|||
await submit_task(parent_with_modes, "child_required_tool", {})
|
||||
).task_id,
|
||||
)
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"]["result"] == "required result"
|
||||
|
||||
async def test_required_mode_without_task_through_mount(self, parent_with_modes):
|
||||
|
|
@ -417,6 +434,7 @@ class TestMiddlewareWithMountedTasks:
|
|||
async with running_task_server(parent):
|
||||
created = await submit_task(parent, "c_gc_compute", {"x": 5})
|
||||
final = await wait_for_task(parent, created.task_id)
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"]["result"] == 10
|
||||
|
||||
assert calls == ["parent:before", "parent:after", "grandchild:tool"]
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@ async def test_same_client_can_access_all_its_tasks(task_server: FastMCP):
|
|||
second = await run_task(
|
||||
task_server, "secret_tool", {"data": "second"}, access_token=token
|
||||
)
|
||||
assert first.result is not None
|
||||
assert "first" in first.result["content"][0]["text"]
|
||||
assert second.result is not None
|
||||
assert "second" in second.result["content"][0]["text"]
|
||||
|
||||
|
||||
|
|
@ -55,6 +57,7 @@ async def test_unauthenticated_client_can_access_its_tasks(task_server: FastMCP)
|
|||
"""An anonymous caller can resolve tasks in the anonymous keyspace."""
|
||||
async with running_task_server(task_server):
|
||||
final = await run_task(task_server, "secret_tool", {"data": "hello"})
|
||||
assert final.result is not None
|
||||
assert "hello" in final.result["content"][0]["text"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ async def test_task_tool_coerces_model_arguments():
|
|||
final = await run_task(mcp, "inspect_items", arguments)
|
||||
|
||||
assert sync_result.structured_content == expected
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == expected
|
||||
|
||||
|
||||
|
|
@ -99,6 +100,7 @@ async def test_task_arguments_are_coerced_like_sync_path():
|
|||
async with running_task_server(mcp):
|
||||
final = await run_task(mcp, "square", {"n": "1"})
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": 1}
|
||||
|
||||
|
||||
|
|
@ -137,6 +139,7 @@ async def test_valid_argument_submits_under_strict_validation():
|
|||
async with running_task_server(mcp):
|
||||
final = await run_task(mcp, "square", {"n": 4})
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": 16}
|
||||
|
||||
|
||||
|
|
@ -202,6 +205,7 @@ async def test_tool_task_executes_in_background():
|
|||
finish.set()
|
||||
final = await wait_for_task(mcp, created.task_id)
|
||||
assert final.status == "completed"
|
||||
assert final.result is not None
|
||||
assert final.result["structuredContent"] == {"result": "completed"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from fastmcp_tasks.models import (
|
|||
CancelTaskResult,
|
||||
CreateTaskResult,
|
||||
GetTaskResult,
|
||||
TaskStatus,
|
||||
UpdateTaskResult,
|
||||
)
|
||||
from jsonschema import Draft202012Validator
|
||||
|
|
@ -72,10 +73,10 @@ def test_create_task_result_matches_schema():
|
|||
("cancelled", {}),
|
||||
],
|
||||
)
|
||||
def test_get_task_result_matches_schema(status: str, payload: dict[str, Any]):
|
||||
def test_get_task_result_matches_schema(status: TaskStatus, payload: dict[str, Any]):
|
||||
result = GetTaskResult(
|
||||
task_id="t1",
|
||||
status=status, # type: ignore[arg-type]
|
||||
status=status,
|
||||
created_at=_ISO,
|
||||
last_updated_at=_ISO,
|
||||
ttl_ms=900000,
|
||||
|
|
@ -111,9 +112,7 @@ def test_null_ttl_is_permitted_by_schema():
|
|||
)
|
||||
dumped = result.model_dump(by_alias=True, mode="json", exclude_none=False)
|
||||
# Drop the other None optionals the runner would also drop, keeping ttlMs=null.
|
||||
dumped = {
|
||||
k: v for k, v in dumped.items() if v is not None or k == "ttlMs"
|
||||
}
|
||||
dumped = {k: v for k, v in dumped.items() if v is not None or k == "ttlMs"}
|
||||
_validate("CreateTaskResult", dumped)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
import contextlib
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from fastmcp_tasks.handlers import tasks_cancel, tasks_get, tasks_update
|
||||
from fastmcp_tasks.models import (
|
||||
|
|
@ -37,6 +37,7 @@ from fastmcp_tasks.models import (
|
|||
from mcp.server.auth.middleware.auth_context import auth_context_var
|
||||
from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.session import ServerSession
|
||||
from mcp_types import CLIENT_CAPABILITIES_META_KEY
|
||||
|
||||
from fastmcp.server.auth import AccessToken
|
||||
|
|
@ -91,7 +92,7 @@ def _opted_in_request(
|
|||
"_meta": opt_in_meta(settings),
|
||||
}
|
||||
srctx = ServerRequestContext(
|
||||
session=SimpleNamespace(),
|
||||
session=cast(ServerSession, SimpleNamespace()),
|
||||
lifespan_context={},
|
||||
protocol_version="2026-07-28",
|
||||
method="tools/call",
|
||||
|
|
@ -202,9 +203,7 @@ async def run_task(
|
|||
timeout: float = 5.0,
|
||||
) -> GetTaskResult:
|
||||
"""Submit a task and wait for it to reach a terminal state."""
|
||||
created = await submit_task(
|
||||
server, name, arguments, access_token=access_token
|
||||
)
|
||||
created = await submit_task(server, name, arguments, access_token=access_token)
|
||||
return await wait_for_task(
|
||||
server, created.task_id, access_token=access_token, timeout=timeout
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue