v3-notes/task-meta-parameter.md
This document captures the design decision to add explicit task_meta parameters to component execution methods, replacing context variable-based task routing.
Background task execution used context variables (_task_metadata, _docket_fn_key) to pass task metadata through the call stack. This was implicit and had several issues:
fn_key was enriched in 9 different places (component methods + provider wrappers)call_tool()Add explicit task_meta: TaskMeta | None parameters to:
FastMCP.call_tool(), FastMCP.read_resource(), FastMCP.render_prompt()Tool._run(), Resource._read(), Prompt._render(), ResourceTemplate._read()from fastmcp.server.tasks import TaskMeta
# Explicit background execution
result = await server.call_tool("my_tool", {"arg": "value"}, task_meta=TaskMeta(ttl=300))
# Returns CreateTaskResult for background, ToolResult for sync
Previously, fn_key (the Docket registry key) was set in 9 places:
Component methods (5):
Tool._run()Resource._read()ResourceTemplate._read() (2 places)Prompt._render()Provider wrappers (4):
FastMCPProviderTool._run()FastMCPProviderResource._read()FastMCPProviderPrompt._render()FastMCPProviderResourceTemplate._read()Now, fn_key is set in 3 places (server methods only):
# In call_tool(), after finding the tool:
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=tool.key)
# In read_resource(), after finding resource or template:
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=resource.key) # or template.key
# In render_prompt(), after finding the prompt:
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=prompt.key)
For mounted servers, provider.get_tool(name) returns a FastMCPProviderTool whose .key is already namespaced (e.g., "tool:child_multiply"). So setting fn_key = tool.key in the parent server gives the correct namespaced key.
When the provider wrapper delegates to the child server, fn_key is already set, so the child server won't override it.
Each method uses @overload to provide correct return types:
@overload
async def call_tool(
self, name: str, arguments: dict[str, Any], *, task_meta: None = None
) -> ToolResult: ...
@overload
async def call_tool(
self, name: str, arguments: dict[str, Any], *, task_meta: TaskMeta
) -> ToolResult | mcp.types.CreateTaskResult: ...
A key fix from #2663: background tasks now properly pass through all middleware stacks before being submitted to Docket. Previously, background task submission bypassed middleware entirely.
The flow is now:
call_tool, etc.) finds component via providertask_meta.fn_key with component key_run()/_read()/_render() is calledcheck_background_task() submits to Docket if task_meta presentFor mounted servers, the wrapper components delegate to the child server, which runs the child's middleware before the actual execution or Docket submission.
_task_metadata context variable_docket_fn_key context variableget_task_metadata() functionkey parameter in check_background_task() (backwards compat fallback)task_meta for call_tool()task_meta for read_resource()task_meta for render_prompt() + fn_key centralization