Enable background task support for custom component subclasses#2657
Enable background task support for custom component subclasses#2657
Conversation
Refactors docket/background task support to be encapsulated within each component rather than requiring external coordination: - Move task_config to FastMCPComponent base class (default: forbidden) - Add register_with_docket(docket) method that components use to register themselves, checking task_config internally - Add add_to_docket() method that handles component-specific calling conventions (splatted kwargs vs positional dict) - Simplify server registration to just call component.register_with_docket() - Update task handlers to use component.add_to_docket() This enables custom Tool/Resource/Prompt subclasses to support background tasks by setting task_config and optionally overriding the docket methods.
4230ddd to
1a1a25a
Compare
WalkthroughThis PR introduces a uniform docket integration pattern across Tool, Resource, Prompt, and their Template variants by adding Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 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
🧹 Nitpick comments (1)
src/fastmcp/resources/template.py (1)
235-239: Minor formatting: extra space in method call.Line 239 has inconsistent spacing
( params)- there's an extra space after the opening parenthesis.🔎 Proposed fix
async def add_to_docket( # type: ignore[override] self, docket: Docket, params: dict[str, Any], **kwargs: Any ) -> Execution: """Schedule this template for background execution via docket.""" - return await docket.add(self.key, **kwargs)( params) + return await docket.add(self.key, **kwargs)(params)
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
tests/server/middleware/test_logging.pyis excluded by none and included by none
📒 Files selected for processing (8)
docs/servers/tasks.mdx(1 hunks)src/fastmcp/prompts/prompt.py(3 hunks)src/fastmcp/resources/resource.py(3 hunks)src/fastmcp/resources/template.py(3 hunks)src/fastmcp/server/server.py(4 hunks)src/fastmcp/server/tasks/handlers.py(3 hunks)src/fastmcp/tools/tool.py(3 hunks)src/fastmcp/utilities/components.py(3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
docs/**/*.mdx
📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)
docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...
Files:
docs/servers/tasks.mdx
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/server.pysrc/fastmcp/utilities/components.pysrc/fastmcp/server/tasks/handlers.pysrc/fastmcp/tools/tool.pysrc/fastmcp/resources/resource.pysrc/fastmcp/resources/template.pysrc/fastmcp/prompts/prompt.py
🧠 Learnings (1)
📚 Learning: 2025-12-17T03:06:14.522Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T03:06:14.522Z
Learning: Applies to src/fastmcp/**/*.py : Write Python code with ≥3.10 type annotations required throughout
Applied to files:
src/fastmcp/utilities/components.py
🧬 Code graph analysis (7)
src/fastmcp/server/server.py (4)
src/fastmcp/resources/resource.py (4)
Resource(137-294)register_with_docket(284-288)register_with_docket(382-390)key(280-282)src/fastmcp/resources/template.py (4)
ResourceTemplate(96-239)register_with_docket(229-233)register_with_docket(297-305)key(225-227)src/fastmcp/prompts/prompt.py (2)
register_with_docket(236-240)register_with_docket(474-482)src/fastmcp/tools/tool.py (2)
register_with_docket(243-247)register_with_docket(430-438)
src/fastmcp/utilities/components.py (2)
src/fastmcp/server/tasks/config.py (1)
TaskConfig(19-89)src/fastmcp/resources/resource.py (3)
register_with_docket(284-288)register_with_docket(382-390)add_to_docket(290-294)
src/fastmcp/server/tasks/handlers.py (7)
src/fastmcp/prompts/prompt.py (2)
add_to_docket(242-246)add_to_docket(484-491)src/fastmcp/resources/resource.py (2)
add_to_docket(290-294)key(280-282)src/fastmcp/resources/template.py (4)
add_to_docket(235-239)add_to_docket(307-314)key(225-227)ResourceTemplate(96-239)src/fastmcp/tools/tool.py (2)
add_to_docket(249-253)add_to_docket(440-447)src/fastmcp/utilities/components.py (2)
add_to_docket(133-153)key(69-77)src/fastmcp/server/context.py (1)
fastmcp(169-174)src/fastmcp/server/low_level.py (2)
fastmcp(43-48)fastmcp(144-149)
src/fastmcp/tools/tool.py (5)
src/fastmcp/server/server.py (2)
docket(389-394)run(550-569)src/fastmcp/prompts/prompt.py (4)
register_with_docket(236-240)register_with_docket(474-482)add_to_docket(242-246)add_to_docket(484-491)src/fastmcp/resources/resource.py (4)
register_with_docket(284-288)register_with_docket(382-390)key(280-282)add_to_docket(290-294)src/fastmcp/resources/template.py (5)
register_with_docket(229-233)register_with_docket(297-305)key(225-227)add_to_docket(235-239)add_to_docket(307-314)src/fastmcp/utilities/components.py (3)
register_with_docket(125-131)key(69-77)add_to_docket(133-153)
src/fastmcp/resources/resource.py (5)
src/fastmcp/server/server.py (1)
docket(389-394)src/fastmcp/resources/template.py (5)
register_with_docket(229-233)register_with_docket(297-305)read(173-177)read(266-295)key(225-227)src/fastmcp/tools/tool.py (2)
register_with_docket(243-247)register_with_docket(430-438)src/fastmcp/utilities/components.py (2)
register_with_docket(125-131)key(69-77)src/fastmcp/server/proxy.py (1)
read(408-433)
src/fastmcp/resources/template.py (3)
src/fastmcp/prompts/prompt.py (2)
register_with_docket(236-240)register_with_docket(474-482)src/fastmcp/resources/resource.py (3)
register_with_docket(284-288)register_with_docket(382-390)key(280-282)src/fastmcp/tools/tool.py (2)
register_with_docket(243-247)register_with_docket(430-438)
src/fastmcp/prompts/prompt.py (6)
src/fastmcp/server/server.py (1)
docket(389-394)src/fastmcp/resources/resource.py (4)
register_with_docket(284-288)register_with_docket(382-390)key(280-282)add_to_docket(290-294)src/fastmcp/resources/template.py (5)
register_with_docket(229-233)register_with_docket(297-305)key(225-227)add_to_docket(235-239)add_to_docket(307-314)src/fastmcp/tools/tool.py (4)
register_with_docket(243-247)register_with_docket(430-438)add_to_docket(249-253)add_to_docket(440-447)src/fastmcp/utilities/components.py (3)
register_with_docket(125-131)key(69-77)add_to_docket(133-153)src/fastmcp/server/proxy.py (1)
render(575-587)
🪛 Ruff (0.14.8)
src/fastmcp/utilities/components.py
134-134: Unused method argument: docket
(ARG002)
134-134: Unused method argument: args
(ARG002)
134-134: Unused method argument: kwargs
(ARG002)
147-150: 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.10 on ubuntu-latest
- GitHub Check: Run tests: Python 3.13 on ubuntu-latest
- GitHub Check: Run tests: Python 3.10 on windows-latest
- GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (20)
docs/servers/tasks.mdx (1)
224-241: LGTM! Clear documentation for custom component subclasses.The new section effectively documents how to enable background task support for custom subclasses by setting
task_config. The code example is clear and demonstrates the pattern correctly.One minor consideration: the example shows
task_configas a class attribute. This works because Pydantic will use it as a default value, but users should be aware this creates a shared default across all instances. SinceTaskConfigis immutable, this is safe behavior.src/fastmcp/utilities/components.py (2)
63-66: LGTM! Task config field with appropriate default.The
task_configfield withmode="forbidden"as the default ensures backward compatibility - existing components won't suddenly support background tasks unless explicitly configured.The use of
default_factory=lambda: TaskConfig(mode="forbidden")is correct for Pydantic models to avoid shared mutable state issues.
125-153: LGTM! Base class docket integration methods are well-designed.The two-method pattern provides clear extension points:
register_with_docket: No-op base that subclasses override to register their callableadd_to_docket: Guard against forbidden mode + NotImplementedError for unimplemented subclassesThe static analysis hints (ARG002, TRY003) are false positives here - this is intentionally a base class with unused parameters that subclasses will use.
src/fastmcp/server/tasks/handlers.py (3)
103-104: LGTM! Clean refactoring to component-encapsulated docket scheduling.The change from direct
docket.add()calls totool.add_to_docket()properly encapsulates the docket scheduling logic within each component, allowing FunctionTool to splat arguments while Tool passes them as a dict.
206-207: LGTM! Consistent pattern for prompt docket scheduling.Same clean encapsulation pattern as tools - the prompt handles its own calling conventions internally.
307-314: LGTM! Proper handling of Resource vs ResourceTemplate.The conditional correctly distinguishes between:
ResourceTemplate: requiresparamsdict extracted from URI matchingResource: no params needed, justkeyThe inline import of
ResourceTemplateandmatch_uri_templateavoids circular import issues.src/fastmcp/tools/tool.py (2)
243-253: LGTM! Tool docket integration with correct calling convention.The implementation correctly:
- Guards registration when mode is "forbidden"
- Registers
self.runwhich expectsarguments: dict[str, Any]- Passes arguments as a single positional arg in
add_to_docket:docket.add(key)(arguments)This matches the
Tool.run(self, arguments: dict)signature.
430-447: LGTM! FunctionTool correctly splats arguments for the underlying function.Key distinction from base
Tool:
- Registers
self.fn(the user's function with Depends parameters for docket DI)- Splats arguments:
docket.add(key)(**arguments)sincefnexpects keyword argumentsThe docstring clearly explains why FunctionTool differs from Tool. The
# type: ignore[override]is appropriate since the method signature intentionally differs.src/fastmcp/server/server.py (4)
419-431: LGTM! Unified docket registration is cleaner and more maintainable.The refactoring from type-specific registration branches to a uniform
component.register_with_docket(docket)call:
- Simplifies the code significantly
- Each component encapsulates its own registration logic and task_config check
- Makes it easy to add new component types without modifying this code
The comment accurately describes the behavior: components check
task_configinternally and no-op if forbidden.
434-444: LGTM! Provider component registration follows the same unified pattern.Consistent with local component registration - providers' task-enabled components use the same
register_with_docket()API.
629-633: LGTM! Resource task routing correctly delegates to handler.The routing logic properly extracts task metadata and delegates to
handle_resource_as_task, which will use the component'sadd_to_docket()method internally.
1455-1460: LGTM! Tool task routing with clear comments.The comment correctly notes that ProxyTool has
mode="forbidden"and won't enter this branch. The delegation tohandle_tool_as_taskmaintains clean separation of concerns.src/fastmcp/prompts/prompt.py (3)
9-15: LGTM!The TYPE_CHECKING guard pattern for
DocketandExecutionimports is correctly implemented, avoiding runtime import overhead while maintaining type hints. This is consistent with the pattern used in other component files (resource.py, template.py).
236-246: LGTM!The
register_with_docketandadd_to_docketimplementations forPromptare consistent with the established pattern inToolandResourceclasses. The basePromptcorrectly registersself.render(the high-level method) rather than the underlying function, and theadd_to_docketsignature properly acceptsarguments: dict[str, Any] | Nonematching the prompt's render interface.
473-491: LGTM!The
FunctionPromptimplementation correctly:
- Registers
self.fn(the wrapped user function) instead ofself.renderto preserve Depends parameters for docket resolution- Splats arguments via
**(arguments or {})since the underlying function expects keyword arguments- Uses
# type: ignore[arg-type]appropriately for the callable type mismatchThis pattern is consistent with
FunctionTool(tool.py lines 439-446) andFunctionResourceimplementations.src/fastmcp/resources/template.py (2)
8-15: LGTM!TYPE_CHECKING guard correctly implemented for
DocketandExecutionimports, consistent with the pattern across all component files.
297-314: LGTM!The
FunctionResourceTemplateimplementation correctly:
- Registers
self.fnto preserve Depends parameters for docket resolution- Splats params via
**paramssince the underlying function expects keyword arguments- Provides clear docstrings explaining the difference from base
ResourceTemplateThis is consistent with the established pattern in
FunctionTool,FunctionPrompt, andFunctionResource.src/fastmcp/resources/resource.py (3)
9-15: LGTM!TYPE_CHECKING guard correctly implemented for
DocketandExecutionimports, maintaining consistency across all component files.
284-294: LGTM!The
Resourcedocket integration correctly:
- Registers
self.read(the public read method) with the resource's URI as the key- Calls
docket.add(self.key, **kwargs)()with no arguments sinceResource.read()takes no parametersThis is consistent with the pattern established in other component base classes.
381-390: LGTM!The
FunctionResource.register_with_docketcorrectly registersself.fn(the wrapped user function) to preserve Depends parameters for docket resolution, consistent with other function-based component implementations.Note:
FunctionResourceappropriately inheritsadd_to_docketfromResourcesince bothread()and the wrappedfntake no arguments—no override is needed.
Custom
Tool,Resource,ResourceTemplate, andPromptsubclasses can now support background task execution. Previously, only decorator-based components (@mcp.tool(task=True)) could use docket.Each component now encapsulates its docket integration via two methods:
register_with_docket(docket)- Registers the component with docket, checkingtask_configinternallyadd_to_docket(docket, ...)- Handles component-specific calling conventions for scheduling executionCustom subclasses enable task support by setting
task_config:The
task_configfield has been elevated toFastMCPComponent(default:mode="forbidden"), so all components inherit it.