Use CreateTaskResult for background task creation#2660
Conversation
Move result conversion logic to components (convert_result methods) and return proper CreateTaskResult SDK type from task handlers. Consolidates MCP protocol handler overrides into server.py with documentation.
WalkthroughThis PR refactors SEP-1686 task support across client and server flows. Clients now send TaskMetadata TTL and use RootModel unions (CreateTaskResult | specific-result) to either register server_task_id or return synthetic immediate-result tasks; arguments are serialized to strings (non-strings JSON-encoded). Tool/prompt/resource classes gain convert_result methods to centralize result normalization. The server removes the converters module, adds custom read/get handlers, registers task protocol endpoints, and updates task handlers to consistently emit mcp.types.CreateTaskResult with structured Task fields and ISO timestamps. Possibly related PRs
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/fastmcp/server/tasks/handlers.py (1)
237-255: Add type annotation forresourceparameter.The
resourceparameter lacks a type annotation, relying only on a comment. Per coding guidelines, Python 3.10+ type annotations are required throughout.🔎 Proposed fix
+from fastmcp.resources.resource import Resource +from fastmcp.resources.template import ResourceTemplate + async def handle_resource_as_task( server: FastMCP, uri: str, - resource, # Resource or ResourceTemplate + resource: Resource | ResourceTemplate, task_meta: dict[str, Any], ) -> mcp.types.CreateTaskResult:Note: The imports may already exist elsewhere or need adjustment based on actual module structure. The import at line 306 suggests
ResourceTemplateis imported locally, so you may need to move that import to the top level.
🧹 Nitpick comments (2)
src/fastmcp/client/client.py (2)
909-930: Consider movingRootModelimport to module level.
RootModelis imported inside multiple methods (_read_resource_as_task,_get_prompt_as_task,_call_tool_as_task). Moving it to the top-level imports would avoid repeated import overhead and improve code clarity.🔎 Proposed fix at module level
Add to the existing pydantic imports near the top of the file:
-from pydantic import AnyUrl +from pydantic import AnyUrl, RootModelThen remove the local imports from lines 909-910, 1126-1127, and 1480.
1129-1147: Argument serialization is duplicated.The argument serialization logic (lines 1129-1139) is duplicated from
get_prompt_mcp(lines 1020-1030). Consider extracting to a helper method for DRY compliance, though this is minor given the limited scope.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
tests/server/tasks/test_task_config_modes.pyis excluded by none and included by none
📒 Files selected for processing (8)
src/fastmcp/client/client.py(4 hunks)src/fastmcp/prompts/prompt.py(2 hunks)src/fastmcp/resources/resource.py(1 hunks)src/fastmcp/server/server.py(4 hunks)src/fastmcp/server/tasks/converters.py(0 hunks)src/fastmcp/server/tasks/handlers.py(12 hunks)src/fastmcp/server/tasks/protocol.py(1 hunks)src/fastmcp/tools/tool.py(3 hunks)
💤 Files with no reviewable changes (1)
- src/fastmcp/server/tasks/converters.py
🧰 Additional context used
📓 Path-based instructions (1)
src/fastmcp/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types
Files:
src/fastmcp/prompts/prompt.pysrc/fastmcp/resources/resource.pysrc/fastmcp/client/client.pysrc/fastmcp/server/server.pysrc/fastmcp/tools/tool.pysrc/fastmcp/server/tasks/handlers.pysrc/fastmcp/server/tasks/protocol.py
🧠 Learnings (1)
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: Review and update related Manager classes (ToolManager, ResourceManager, PromptManager) when modifying MCP object definitions
Applied to files:
src/fastmcp/server/server.pysrc/fastmcp/server/tasks/handlers.py
🧬 Code graph analysis (4)
src/fastmcp/prompts/prompt.py (3)
src/fastmcp/resources/resource.py (1)
convert_result(381-387)src/fastmcp/tools/tool.py (2)
convert_result(243-248)convert_result(405-443)src/fastmcp/exceptions.py (1)
PromptError(22-23)
src/fastmcp/resources/resource.py (2)
src/fastmcp/prompts/prompt.py (3)
convert_result(211-216)convert_result(447-485)from_value(88-106)src/fastmcp/tools/tool.py (2)
convert_result(243-248)convert_result(405-443)
src/fastmcp/server/server.py (1)
src/fastmcp/server/tasks/handlers.py (3)
handle_tool_as_task(27-131)handle_resource_as_task(237-340)handle_prompt_as_task(134-234)
src/fastmcp/tools/tool.py (4)
src/fastmcp/prompts/prompt.py (2)
convert_result(211-216)convert_result(447-485)src/fastmcp/resources/resource.py (1)
convert_result(381-387)src/fastmcp/client/tasks.py (4)
result(202-207)result(336-393)result(427-458)result(497-551)src/fastmcp/utilities/types.py (1)
Audio(307-362)
🪛 Ruff (0.14.8)
src/fastmcp/prompts/prompt.py
445-445: Avoid specifying long messages outside the exception class
(TRY003)
479-479: Avoid specifying long messages outside the exception class
(TRY003)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Run tests: Python 3.13 on ubuntu-latest
- GitHub Check: Run tests: Python 3.10 on ubuntu-latest
- GitHub Check: Run tests with lowest-direct dependencies
- GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (15)
src/fastmcp/resources/resource.py (1)
379-387: LGTM! Clean centralization of conversion logic.The
convert_resultmethod correctly delegates toResourceContent.from_valuewith the resource'smime_type, enabling consistent handling of raw values from Docket background execution.Note: Unlike
ToolandPromptbase classes which define abstractconvert_result()methods, the baseResourceclass doesn't. This seems intentional since baseResource.read()doesn't callconvert_result()and onlyFunctionResourceneeds it for the Docket path.src/fastmcp/prompts/prompt.py (2)
211-216: LGTM! Consistent abstract method pattern.The base
Promptclass now defines an abstractconvert_result()that subclasses must implement, matching the pattern inTool. This ensures a consistent interface for the task-based execution flow.
447-485: Well-structured conversion logic.The
convert_resultmethod properly handles all expected input types:
PromptMessageinstances pass through- Strings are wrapped with
TextContent- Other types are JSON-serialized with
pydantic_core.to_jsonThe error handling correctly wraps conversion failures in
PromptErrorfor consistent exception propagation.src/fastmcp/tools/tool.py (2)
243-248: LGTM! Abstract method defines the contract.Consistent with the base
Promptclass pattern, forcing subclasses to implement their specific conversion logic.
405-443: Correct conversion flow with proper schema handling.The implementation correctly handles all cases:
- Pass-through for existing
ToolResultinstances- Unstructured-only for MCP content types (
ContentBlock,Audio,Image,File)- Attempts dict serialization for structured content when no schema
- Respects
x-fastmcp-wrap-resultflag for non-object return typesThe
raw_valueusage at line 442 is safe because schema validation (lines 373-377) ensuresoutput_schemais an object type, and non-object types get thewrap_resultflag set, forcing the{"result": raw_value}wrapper.src/fastmcp/server/tasks/protocol.py (1)
228-278: Clean per-type result conversion with proper metadata injection.The refactored handler correctly routes to component-specific conversion:
- Tools: Uses
tool.convert_result()→to_mcp_result()with proper handling of all three result shapes (CallToolResult, tuple, or content list)- Prompts: Uses
prompt.convert_result()→to_mcp_prompt_result()- Resources: Direct
ResourceContent.from_value()for raw valuesThe related-task metadata is consistently injected across all paths, aligning with SEP-1686 requirements.
src/fastmcp/server/server.py (3)
566-599: Clear documentation of SDK asymmetry.The docstring at lines 567-579 explains the design decision well: tools use the SDK decorator for input validation and CreateTaskResult support, while resources and prompts require custom handlers due to SDK limitations. This is a pragmatic approach to work around SDK constraints.
1258-1347: Well-structured task routing for tools.The handler correctly:
- Extracts task metadata from the SDK request context
- Enforces
mode="required"by raisingMcpErrorwhen task metadata is missing- Routes to
handle_tool_as_taskwhen task support is enabled- Returns an error
CallToolResultwithisError=Truewhenmode="forbidden"but task was requested- Falls through to synchronous execution otherwise
The
returned_immediately: Truemetadata (line 1337) correctly signals to clients that no background task was created.
1349-1411: Consistent task-aware handlers for resources and prompts.Both handlers follow the same pattern:
- Extract task metadata from request context
- Check component's
task_config.mode- Route to background execution or raise appropriate errors
- Fall through to synchronous execution
Minor inconsistency to verify: When
mode="forbidden"but task is requested:
- Tools return an error
CallToolResultwithisError=True(lines 1326-1339)- Resources/Prompts raise
McpError(lines 1399-1404, 1466-1472)This difference may be intentional since
CallToolResulthas anisErrorfield while resource/prompt results don't have an equivalent. Please confirm this asymmetry is expected.Also applies to: 1413-1476
src/fastmcp/server/tasks/handlers.py (3)
27-46: LGTM!The handler signature and return type are properly updated to use
mcp.types.CreateTaskResult. The docstring accurately reflects the new return type.
120-131: LGTM!The
CreateTaskResultconstruction properly initializes the Task withstatus="working"and correct timestamp handling. The TTL conversion to milliseconds and pollInterval are appropriate.
134-151: LGTM!The handler follows the same pattern as
handle_tool_as_taskwith proper return type annotation and documentation.src/fastmcp/client/client.py (3)
932-950: LGTM!The branching logic correctly handles both
CreateTaskResult(background task accepted) andReadResourceResult(graceful degradation). Task registration and synthetic ID generation are properly implemented.
1303-1310: LGTM!The simplified implementation correctly delegates to
session.call_toolwith all relevant parameters, removing the previous conditional path for task metadata handling.
1469-1507: LGTM!The implementation correctly constructs the request with
TaskMetadata, handles the union response type, and appropriately parses the result for graceful degradation using_parse_call_tool_result.
- Add type annotation for resource parameter in handle_resource_as_task - Move RootModel import to module level in client.py
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/fastmcp/server/tasks/handlers.py (1)
29-133: Remove unusedtask_metaparameter or respect client-requested TTL.The
handle_tool_as_taskfunction receivestask_meta(containing client-requested TTL) but ignores it, instead usingdocket.execution_ttlexclusively (lines 78–80, 130). Either remove the unused parameter or implement client TTL support. If server-side TTL policy is intentional, document the override behavior.
🧹 Nitpick comments (2)
src/fastmcp/client/client.py (1)
1125-1136: Consider extracting argument serialization to reduce duplication.The argument serialization logic (lines 1125-1136) is duplicated from
get_prompt_mcp(lines 1017-1028). Consider extracting a helper method like_serialize_prompt_arguments()to maintain DRY.🔎 Suggested helper extraction
def _serialize_prompt_arguments( self, arguments: dict[str, Any] | None ) -> dict[str, str] | None: """Serialize prompt arguments for MCP protocol.""" if not arguments: return None serialized: dict[str, str] = {} for key, value in arguments.items(): if isinstance(value, str): serialized[key] = value else: serialized[key] = pydantic_core.to_json(value).decode("utf-8") return serializedThen use in both methods:
serialized_arguments = self._serialize_prompt_arguments(arguments)src/fastmcp/server/tasks/handlers.py (1)
239-244: Consider removing unusedserverparameter or documenting future intent.Static analysis correctly flags that
serveris unused inhandle_resource_as_task. Unlikehandle_tool_as_taskandhandle_prompt_as_taskwhich useserver.get_tool()/server.get_prompt()to look up components, this handler receivesresourcedirectly.If keeping for signature consistency, prefix with underscore (
_server) to suppress the warning and signal intentional non-use. Same applies totask_meta.🔎 Suggested fix to suppress lint warnings
async def handle_resource_as_task( - server: FastMCP, + _server: FastMCP, # Unused, kept for signature consistency uri: str, resource: Resource | ResourceTemplate, - task_meta: dict[str, Any], + _task_meta: dict[str, Any], # TTL policy from docket.execution_ttl ) -> mcp.types.CreateTaskResult:
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/fastmcp/client/client.py(5 hunks)src/fastmcp/server/tasks/handlers.py(12 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/fastmcp/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types
Files:
src/fastmcp/client/client.pysrc/fastmcp/server/tasks/handlers.py
🧠 Learnings (2)
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: Review and update related Manager classes (ToolManager, ResourceManager, PromptManager) when modifying MCP object definitions
Applied to files:
src/fastmcp/server/tasks/handlers.py
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: When implementing changes that affect MCP object types (Tools, Resources, Resource Templates, or Prompts), such as adding tags or importing, ensure changes are adopted, applied, and tested on all four object types
Applied to files:
src/fastmcp/server/tasks/handlers.py
🧬 Code graph analysis (1)
src/fastmcp/client/client.py (1)
src/fastmcp/client/tasks.py (8)
ResourceTask(461-551)task_id(105-107)PromptTask(396-458)result(202-207)result(336-393)result(427-458)result(497-551)ToolTask(294-393)
🪛 Ruff (0.14.8)
src/fastmcp/server/tasks/handlers.py
240-240: Unused function argument: server
(ARG001)
243-243: Unused function argument: task_meta
(ARG001)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Run tests: Python 3.10 on ubuntu-latest
- GitHub Check: Run tests: Python 3.10 on windows-latest
- GitHub Check: Run tests with lowest-direct dependencies
- GitHub Check: Run tests: Python 3.13 on ubuntu-latest
🔇 Additional comments (7)
src/fastmcp/client/client.py (4)
33-33: LGTM: RootModel import for union response handling.The
RootModelimport from Pydantic enables discriminated union handling for SEP-1686 task responses, allowing the client to distinguish betweenCreateTaskResultand immediate results.
910-948: LGTM: Clean refactor to RootModel union for resource task handling.The pattern correctly:
- Builds a request with
TaskMetadata(ttl=ttl)- Uses
RootModelunion to handle bothCreateTaskResultandReadResourceResult- Properly extracts
server_task_idfromraw_result.task.taskIdfor background tasks- Falls back to synthetic task ID with immediate result for graceful degradation
1299-1306: LGTM: Simplified call_tool_mcp implementation.Clean consolidation that properly forwards the
metaparameter tosession.call_tool(), aligning with the SDK's native task support.
1465-1501: LGTM: Consistent task handling pattern for tools.The implementation correctly:
- Builds
CallToolRequestwith embeddedTaskMetadata- Uses
RootModelunion for discriminated response handling- Parses
CallToolResultvia_parse_call_tool_resultbefore creatingToolTaskwith immediate result- Maintains consistency with resource and prompt task handlers
src/fastmcp/server/tasks/handlers.py (3)
21-24: LGTM: Updated TYPE_CHECKING imports for widened type support.The imports correctly support the widened
resourceparameter type (Resource | ResourceTemplate) inhandle_resource_as_task.
136-236: LGTM: Consistent prompt task handler implementation.The handler follows the same pattern as
handle_tool_as_taskwith properCreateTaskResultconstruction. The same TTL consideration from the tool handler applies here.
331-342: LGTM: CorrectCreateTaskResultconstruction per SEP-1686.The
Taskobject includes all required fields:
status="working"as mandated by speccreatedAtandlastUpdatedAtboth set to creation timettlproperly converted to millisecondspollIntervalset to reasonable 1000ms default
Prefix with underscore to suppress lint warnings. Client TTL will be configurable via TaskConfig in the future; keeping parameter for API stability.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/fastmcp/server/tasks/handlers.py (1)
61-62: Remove redundantget_context()call.The context is retrieved on line 61 and used to get
session_id. On line 102,get_context()is called again unnecessarily. Thectxvariable from line 61 should be reused for sending the notification on line 105.This pattern is repeated in all three handler functions (also at lines 170/210 and 276/313).
🔎 Proposed fix
- ctx = get_context() - with suppress(Exception): + with suppress(Exception): # Don't let notification failures break task creation await ctx.session.send_notification(notification) # type: ignore[arg-type]Apply the same fix to
handle_prompt_as_taskandhandle_resource_as_task.Also applies to: 102-105
🧹 Nitpick comments (1)
src/fastmcp/server/tasks/handlers.py (1)
29-137: Consider extracting common task handling logic.The three handler functions share ~80% of their code (task ID generation, timestamp handling, Redis storage, notification sending, subscription spawning, and result construction). Only the component-specific retrieval and docket queueing differ.
Consider extracting the common logic into a shared helper function to improve maintainability and reduce the risk of inconsistent updates.
Potential refactor approach
A shared helper could handle the common workflow:
async def _execute_as_task( component_type: Literal["tool", "prompt", "resource"], component_id: str, add_to_docket_fn: Callable[..., Awaitable[None]], ) -> mcp.types.CreateTaskResult: """Common task execution workflow for all component types.""" # Task ID generation # Timestamp creation # Redis storage # Notification sending # Docket queueing (via callback) # Subscription spawning # Return CreateTaskResult ...Then each handler would be simplified to component-specific logic + call to the helper.
Also applies to: 140-243, 246-352
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/fastmcp/server/tasks/handlers.py(8 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/fastmcp/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/fastmcp/**/*.py: Write Python code with ≥3.10 type annotations required throughout
Never use bare except - be specific with exception types
Files:
src/fastmcp/server/tasks/handlers.py
🧠 Learnings (2)
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: Review and update related Manager classes (ToolManager, ResourceManager, PromptManager) when modifying MCP object definitions
Applied to files:
src/fastmcp/server/tasks/handlers.py
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: When implementing changes that affect MCP object types (Tools, Resources, Resource Templates, or Prompts), such as adding tags or importing, ensure changes are adopted, applied, and tested on all four object types
Applied to files:
src/fastmcp/server/tasks/handlers.py
🧬 Code graph analysis (1)
src/fastmcp/server/tasks/handlers.py (3)
src/fastmcp/server/server.py (2)
resource(2072-2215)FastMCP(158-2952)examples/tasks/client.py (1)
task(83-128)src/fastmcp/client/tasks.py (2)
Task(47-291)status(171-199)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Run tests: Python 3.10 on ubuntu-latest
- GitHub Check: Run tests: Python 3.13 on ubuntu-latest
- GitHub Check: Run tests with lowest-direct dependencies
- GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (2)
src/fastmcp/server/tasks/handlers.py (2)
21-22: LGTM: Clean type imports and signature updates.The TYPE_CHECKING imports avoid circular dependencies, and the
_task_metaparameter is correctly marked as unused while maintaining signature consistency. The docstring clearly explains that server-side TTL policy takes precedence.Also applies to: 33-34, 40-51
126-137: LGTM: Correct CreateTaskResult implementation.The Task object is properly constructed with all required fields per SEP-1686:
- Server-generated taskId
- Initial "working" status
- Timezone-aware timestamps
- TTL correctly converted to milliseconds
- Reasonable pollInterval
The return type aligns with the PR's objective to use the MCP SDK's CreateTaskResult type.
Background tasks previously returned manually-constructed dicts with
_metafields. This refactor properly uses the MCP SDK'sCreateTaskResulttype and moves result conversion logic to the components where it belongs.The SDK's handler decorators have asymmetric capabilities:
call_toolsupports bothCreateTaskResultreturns andvalidate_input, whileread_resourceandget_promptdon't supportCreateTaskResult. We now use the SDK'scall_tooldecorator (which gives us strict input validation) and custom request handlers only for resources/prompts (with documentation explaining this SDK asymmetry).This deletion of
converters.py(-204 lines) and consolidation of handler logic makes the task pipeline match the non-background execution path.