Skip to content

fix: propagate origin_request_id to background task workers#3175

Merged
jlowin merged 2 commits intoPrefectHQ:mainfrom
gfortaine:fix/task-origin-request-id
Feb 22, 2026
Merged

fix: propagate origin_request_id to background task workers#3175
jlowin merged 2 commits intoPrefectHQ:mainfrom
gfortaine:fix/task-origin-request-id

Conversation

@gfortaine
Copy link
Copy Markdown
Contributor

@gfortaine gfortaine commented Feb 13, 2026

Closes #2877. Follows #2905, #2906.

Description

In task mode, tool code runs in a Docket worker where request-scoped context isn’t present. Without an originating request id, background logs and traces lose the ability to reliably correlate work back to the request that queued it.

This PR snapshots the submitting request id as origin_request_id when a task is enqueued, restores it into the worker Context, and uses that value for log correlation (related_request_id) so foreground and background executions stay traceable.

from fastmcp import Context
from fastmcp.server.tasks import TaskConfig

@server.tool(task=TaskConfig(mode="required"))
async def do_work(ctx: Context) -> str:
    await ctx.log("started")  # correlates via ctx.origin_request_id in both modes
    return ctx.origin_request_id or "unknown"

Using AI to generate code: GitHub Copilot (GPT-5.3-Codex).

Contributors Checklist

  • My change closes Passing context to Background Task #2877
  • I have followed the repository's development workflow
  • I have tested my changes manually and by adding relevant tests
  • I have performed all required documentation updates

Review Checklist

  • I have self-reviewed my changes
  • My Pull Request is ready for review

@marvin-context-protocol marvin-context-protocol Bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality. tests labels Feb 13, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 13, 2026

Walkthrough

The changes extend the Context class to support background task execution by adding task_id and origin_request_id parameters to its constructor, along with corresponding properties and an is_background_task indicator. Logging now derives related_request_id from origin_request_id. The dependencies module introduces TaskContextInfo, task session registration (register_task_session / get_task_session) and get_task_context, implements task-aware context restoration in _CurrentContext (including Redis-backed restoration of origin_request_id and access-token snapshots), and adds OptionalCurrentContext for optional DI. Task handlers capture/store origin_request_id and access-token snapshots in Redis and register sessions for background workers.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (310 files):

⚔️ .github/dependabot.yml (content)
⚔️ .github/workflows/marvin-dedupe-issues.yml (content)
⚔️ .github/workflows/marvin-label-triage.yml (content)
⚔️ .github/workflows/run-static.yml (content)
⚔️ .github/workflows/run-tests.yml (content)
⚔️ .github/workflows/update-config-schema.yml (content)
⚔️ .github/workflows/update-sdk-docs.yml (content)
⚔️ AGENTS.md (content)
⚔️ README.md (content)
⚔️ docs/changelog.mdx (content)
⚔️ docs/clients/auth/bearer.mdx (content)
⚔️ docs/clients/auth/oauth.mdx (content)
⚔️ docs/clients/client.mdx (content)
⚔️ docs/clients/elicitation.mdx (content)
⚔️ docs/clients/logging.mdx (content)
⚔️ docs/clients/progress.mdx (content)
⚔️ docs/clients/prompts.mdx (content)
⚔️ docs/clients/resources.mdx (content)
⚔️ docs/clients/roots.mdx (content)
⚔️ docs/clients/sampling.mdx (content)
⚔️ docs/clients/tasks.mdx (content)
⚔️ docs/clients/tools.mdx (content)
⚔️ docs/clients/transports.mdx (content)
⚔️ docs/css/style.css (content)
⚔️ docs/deployment/http.mdx (content)
⚔️ docs/deployment/server-configuration.mdx (content)
⚔️ docs/development/contributing.mdx (content)
⚔️ docs/development/upgrade-guide.mdx (content)
⚔️ docs/development/v3-notes/v3-features.mdx (content)
⚔️ docs/docs.json (content)
⚔️ docs/getting-started/installation.mdx (content)
⚔️ docs/getting-started/quickstart.mdx (content)
⚔️ docs/getting-started/welcome.mdx (content)
⚔️ docs/integrations/auth0.mdx (content)
⚔️ docs/integrations/authkit.mdx (content)
⚔️ docs/integrations/aws-cognito.mdx (content)
⚔️ docs/integrations/azure.mdx (content)
⚔️ docs/integrations/chatgpt.mdx (content)
⚔️ docs/integrations/claude-code.mdx (content)
⚔️ docs/integrations/claude-desktop.mdx (content)
⚔️ docs/integrations/cursor.mdx (content)
⚔️ docs/integrations/descope.mdx (content)
⚔️ docs/integrations/discord.mdx (content)
⚔️ docs/integrations/eunomia-authorization.mdx (content)
⚔️ docs/integrations/fastapi.mdx (content)
⚔️ docs/integrations/gemini-cli.mdx (content)
⚔️ docs/integrations/github.mdx (content)
⚔️ docs/integrations/google.mdx (content)
⚔️ docs/integrations/oci.mdx (content)
⚔️ docs/integrations/scalekit.mdx (content)
⚔️ docs/integrations/supabase.mdx (content)
⚔️ docs/integrations/workos.mdx (content)
⚔️ docs/patterns/cli.mdx (content)
⚔️ docs/python-sdk/fastmcp-cli-cli.mdx (content)
⚔️ docs/python-sdk/fastmcp-cli-install-cursor.mdx (content)
⚔️ docs/python-sdk/fastmcp-cli-install-shared.mdx (content)
⚔️ docs/python-sdk/fastmcp-cli-run.mdx (content)
⚔️ docs/python-sdk/fastmcp-client-auth-oauth.mdx (content)
⚔️ docs/python-sdk/fastmcp-client-client.mdx (content)
⚔️ docs/python-sdk/fastmcp-client-sampling-handlers-anthropic.mdx (content)
⚔️ docs/python-sdk/fastmcp-mcp_config.mdx (content)
⚔️ docs/python-sdk/fastmcp-prompts-function_prompt.mdx (content)
⚔️ docs/python-sdk/fastmcp-prompts-prompt.mdx (content)
⚔️ docs/python-sdk/fastmcp-resources-function_resource.mdx (content)
⚔️ docs/python-sdk/fastmcp-resources-resource.mdx (content)
⚔️ docs/python-sdk/fastmcp-resources-template.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-auth-auth.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-auth-authorization.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-auth-jwt_issuer.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-auth-providers-azure.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-context.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-dependencies.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-low_level.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-middleware-authorization.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-middleware-caching.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-middleware-error_handling.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-providers-__init__.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-providers-aggregate.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-providers-base.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-providers-fastmcp_provider.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-providers-filesystem.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-providers-openapi-components.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-providers-proxy.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-sampling-run.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-server.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-tasks-config.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-tasks-handlers.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-tasks-requests.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-transforms-__init__.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-transforms-namespace.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-transforms-tool_transform.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-transforms-version_filter.mdx (content)
⚔️ docs/python-sdk/fastmcp-server-transforms-visibility.mdx (content)
⚔️ docs/python-sdk/fastmcp-settings.mdx (content)
⚔️ docs/python-sdk/fastmcp-tools-function_tool.mdx (content)
⚔️ docs/python-sdk/fastmcp-tools-tool.mdx (content)
⚔️ docs/python-sdk/fastmcp-tools-tool_transform.mdx (content)
⚔️ docs/python-sdk/fastmcp-utilities-auth.mdx (content)
⚔️ docs/python-sdk/fastmcp-utilities-components.mdx (content)
⚔️ docs/python-sdk/fastmcp-utilities-json_schema.mdx (content)
⚔️ docs/python-sdk/fastmcp-utilities-lifespan.mdx (content)
⚔️ docs/python-sdk/fastmcp-utilities-openapi-formatters.mdx (content)
⚔️ docs/python-sdk/fastmcp-utilities-openapi-parser.mdx (content)
⚔️ docs/python-sdk/fastmcp-utilities-tests.mdx (content)
⚔️ docs/python-sdk/fastmcp-utilities-versions.mdx (content)
⚔️ docs/servers/auth/oauth-proxy.mdx (content)
⚔️ docs/servers/auth/oidc-proxy.mdx (content)
⚔️ docs/servers/auth/remote-oauth.mdx (content)
⚔️ docs/servers/authorization.mdx (content)
⚔️ docs/servers/context.mdx (content)
⚔️ docs/servers/elicitation.mdx (content)
⚔️ docs/servers/icons.mdx (content)
⚔️ docs/servers/lifespan.mdx (content)
⚔️ docs/servers/logging.mdx (content)
⚔️ docs/servers/middleware.mdx (content)
⚔️ docs/servers/pagination.mdx (content)
⚔️ docs/servers/progress.mdx (content)
⚔️ docs/servers/prompts.mdx (content)
⚔️ docs/servers/providers/custom.mdx (content)
⚔️ docs/servers/providers/filesystem.mdx (content)
⚔️ docs/servers/providers/local.mdx (content)
⚔️ docs/servers/providers/mounting.mdx (content)
⚔️ docs/servers/providers/overview.mdx (content)
⚔️ docs/servers/providers/proxy.mdx (content)
⚔️ docs/servers/resources.mdx (content)
⚔️ docs/servers/sampling.mdx (content)
⚔️ docs/servers/server.mdx (content)
⚔️ docs/servers/tasks.mdx (content)
⚔️ docs/servers/telemetry.mdx (content)
⚔️ docs/servers/tools.mdx (content)
⚔️ docs/servers/versioning.mdx (content)
⚔️ docs/servers/visibility.mdx (content)
⚔️ docs/updates.mdx (content)
⚔️ examples/atproto_mcp/README.md (content)
⚔️ examples/auth/github_oauth/client.py (content)
⚔️ examples/mount_example.py (content)
⚔️ examples/sampling/structured_output.py (content)
⚔️ examples/sampling/tool_use.py (content)
⚔️ examples/smart_home/src/smart_home/hub.py (content)
⚔️ examples/smart_home/src/smart_home/lights/hue_utils.py (content)
⚔️ examples/smart_home/src/smart_home/lights/server.py (content)
⚔️ examples/tags_example.py (content)
⚔️ examples/testing_demo/uv.lock (content)
⚔️ justfile (content)
⚔️ loq.toml (content)
⚔️ pyproject.toml (content)
⚔️ scripts/auto_close_duplicates.py (content)
⚔️ src/fastmcp/cli/cli.py (content)
⚔️ src/fastmcp/cli/install/__init__.py (content)
⚔️ src/fastmcp/cli/install/cursor.py (content)
⚔️ src/fastmcp/cli/install/shared.py (content)
⚔️ src/fastmcp/cli/run.py (content)
⚔️ src/fastmcp/client/auth/oauth.py (content)
⚔️ src/fastmcp/client/client.py (content)
⚔️ src/fastmcp/client/sampling/handlers/anthropic.py (content)
⚔️ src/fastmcp/client/sampling/handlers/openai.py (content)
⚔️ src/fastmcp/contrib/component_manager/README.md (content)
⚔️ src/fastmcp/contrib/component_manager/__init__.py (content)
⚔️ src/fastmcp/contrib/component_manager/component_manager.py (content)
⚔️ src/fastmcp/contrib/component_manager/example.py (content)
⚔️ src/fastmcp/contrib/mcp_mixin/example.py (content)
⚔️ src/fastmcp/dependencies.py (content)
⚔️ src/fastmcp/experimental/utilities/openapi/__init__.py (content)
⚔️ src/fastmcp/mcp_config.py (content)
⚔️ src/fastmcp/prompts/function_prompt.py (content)
⚔️ src/fastmcp/prompts/prompt.py (content)
⚔️ src/fastmcp/resources/function_resource.py (content)
⚔️ src/fastmcp/resources/resource.py (content)
⚔️ src/fastmcp/resources/template.py (content)
⚔️ src/fastmcp/server/auth/__init__.py (content)
⚔️ src/fastmcp/server/auth/auth.py (content)
⚔️ src/fastmcp/server/auth/authorization.py (content)
⚔️ src/fastmcp/server/auth/jwt_issuer.py (content)
⚔️ src/fastmcp/server/auth/oidc_proxy.py (content)
⚔️ src/fastmcp/server/auth/providers/azure.py (content)
⚔️ src/fastmcp/server/auth/providers/jwt.py (content)
⚔️ src/fastmcp/server/auth/redirect_validation.py (content)
⚔️ src/fastmcp/server/context.py (content)
⚔️ src/fastmcp/server/dependencies.py (content)
⚔️ src/fastmcp/server/low_level.py (content)
⚔️ src/fastmcp/server/middleware/authorization.py (content)
⚔️ src/fastmcp/server/middleware/caching.py (content)
⚔️ src/fastmcp/server/middleware/error_handling.py (content)
⚔️ src/fastmcp/server/openapi/server.py (content)
⚔️ src/fastmcp/server/providers/__init__.py (content)
⚔️ src/fastmcp/server/providers/aggregate.py (content)
⚔️ src/fastmcp/server/providers/base.py (content)
⚔️ src/fastmcp/server/providers/fastmcp_provider.py (content)
⚔️ src/fastmcp/server/providers/openapi/components.py (content)
⚔️ src/fastmcp/server/providers/openapi/provider.py (content)
⚔️ src/fastmcp/server/providers/proxy.py (content)
⚔️ src/fastmcp/server/sampling/run.py (content)
⚔️ src/fastmcp/server/sampling/sampling_tool.py (content)
⚔️ src/fastmcp/server/server.py (content)
⚔️ src/fastmcp/server/tasks/__init__.py (content)
⚔️ src/fastmcp/server/tasks/config.py (content)
⚔️ src/fastmcp/server/tasks/handlers.py (content)
⚔️ src/fastmcp/server/tasks/requests.py (content)
⚔️ src/fastmcp/server/tasks/subscriptions.py (content)
⚔️ src/fastmcp/server/transforms/__init__.py (content)
⚔️ src/fastmcp/server/transforms/namespace.py (content)
⚔️ src/fastmcp/server/transforms/tool_transform.py (content)
⚔️ src/fastmcp/server/transforms/version_filter.py (content)
⚔️ src/fastmcp/server/transforms/visibility.py (content)
⚔️ src/fastmcp/settings.py (content)
⚔️ src/fastmcp/tools/function_tool.py (content)
⚔️ src/fastmcp/tools/tool.py (content)
⚔️ src/fastmcp/tools/tool_transform.py (content)
⚔️ src/fastmcp/utilities/auth.py (content)
⚔️ src/fastmcp/utilities/cli.py (content)
⚔️ src/fastmcp/utilities/components.py (content)
⚔️ src/fastmcp/utilities/inspect.py (content)
⚔️ src/fastmcp/utilities/json_schema.py (content)
⚔️ src/fastmcp/utilities/lifespan.py (content)
⚔️ src/fastmcp/utilities/openapi/__init__.py (content)
⚔️ src/fastmcp/utilities/openapi/director.py (content)
⚔️ src/fastmcp/utilities/openapi/formatters.py (content)
⚔️ src/fastmcp/utilities/openapi/parser.py (content)
⚔️ src/fastmcp/utilities/tests.py (content)
⚔️ src/fastmcp/utilities/versions.py (content)
⚔️ tests/cli/test_cli.py (content)
⚔️ tests/cli/test_cursor.py (content)
⚔️ tests/cli/test_install.py (content)
⚔️ tests/cli/test_run.py (content)
⚔️ tests/cli/test_server_args.py (content)
⚔️ tests/client/tasks/test_client_prompt_tasks.py (content)
⚔️ tests/client/tasks/test_client_resource_tasks.py (content)
⚔️ tests/client/tasks/test_client_tool_tasks.py (content)
⚔️ tests/client/test_notifications.py (content)
⚔️ tests/client/test_sampling.py (content)
⚔️ tests/client/test_stdio.py (content)
⚔️ tests/conftest.py (content)
⚔️ tests/contrib/test_component_manager.py (content)
⚔️ tests/contrib/test_mcp_mixin.py (content)
⚔️ tests/deprecated/server/test_include_exclude_tags.py (content)
⚔️ tests/deprecated/test_add_tool_transformation.py (content)
⚔️ tests/deprecated/test_deprecated.py (content)
⚔️ tests/deprecated/test_exclude_args.py (content)
⚔️ tests/deprecated/test_import_server.py (content)
⚔️ tests/deprecated/test_openapi_deprecations.py (content)
⚔️ tests/deprecated/test_settings.py (content)
⚔️ tests/deprecated/test_tool_serializer.py (content)
⚔️ tests/integration_tests/auth/test_github_provider_integration.py (content)
⚔️ tests/prompts/test_prompt.py (content)
⚔️ tests/resources/test_resource_template.py (content)
⚔️ tests/server/auth/providers/test_azure.py (content)
⚔️ tests/server/auth/providers/test_discord.py (content)
⚔️ tests/server/auth/providers/test_github.py (content)
⚔️ tests/server/auth/providers/test_google.py (content)
⚔️ tests/server/auth/providers/test_introspection.py (content)
⚔️ tests/server/auth/providers/test_workos.py (content)
⚔️ tests/server/auth/test_authorization.py (content)
⚔️ tests/server/auth/test_enhanced_error_responses.py (content)
⚔️ tests/server/auth/test_jwt_issuer.py (content)
⚔️ tests/server/auth/test_jwt_provider.py (content)
⚔️ tests/server/auth/test_oauth_consent_flow.py (content)
⚔️ tests/server/auth/test_oauth_mounting.py (content)
⚔️ tests/server/auth/test_oauth_proxy_redirect_validation.py (content)
⚔️ tests/server/auth/test_oauth_proxy_storage.py (content)
⚔️ tests/server/auth/test_oidc_proxy.py (content)
⚔️ tests/server/auth/test_redirect_validation.py (content)
⚔️ tests/server/auth/test_remote_auth_provider.py (content)
⚔️ tests/server/http/test_http_dependencies.py (content)
⚔️ tests/server/middleware/test_caching.py (content)
⚔️ tests/server/middleware/test_error_handling.py (content)
⚔️ tests/server/middleware/test_logging.py (content)
⚔️ tests/server/middleware/test_middleware.py (content)
⚔️ tests/server/middleware/test_timing.py (content)
⚔️ tests/server/providers/openapi/test_comprehensive.py (content)
⚔️ tests/server/providers/openapi/test_openapi_features.py (content)
⚔️ tests/server/providers/openapi/test_openapi_performance.py (content)
⚔️ tests/server/providers/openapi/test_performance_comparison.py (content)
⚔️ tests/server/providers/openapi/test_server.py (content)
⚔️ tests/server/providers/proxy/test_proxy_server.py (content)
⚔️ tests/server/providers/proxy/test_stateful_proxy_client.py (content)
⚔️ tests/server/providers/test_base_provider.py (content)
⚔️ tests/server/providers/test_fastmcp_provider.py (content)
⚔️ tests/server/providers/test_local_provider.py (content)
⚔️ tests/server/providers/test_local_provider_prompts.py (content)
⚔️ tests/server/providers/test_local_provider_resources.py (content)
⚔️ tests/server/providers/test_transforming_provider.py (content)
⚔️ tests/server/sampling/test_sampling_tool.py (content)
⚔️ tests/server/tasks/test_task_metadata.py (content)
⚔️ tests/server/tasks/test_task_prompts.py (content)
⚔️ tests/server/tasks/test_task_protocol.py (content)
⚔️ tests/server/tasks/test_task_resources.py (content)
⚔️ tests/server/tasks/test_task_tools.py (content)
⚔️ tests/server/test_context.py (content)
⚔️ tests/server/test_dependencies.py (content)
⚔️ tests/server/test_log_level.py (content)
⚔️ tests/server/test_logging.py (content)
⚔️ tests/server/test_pagination.py (content)
⚔️ tests/server/test_providers.py (content)
⚔️ tests/server/test_server.py (content)
⚔️ tests/server/test_server_lifespan.py (content)
⚔️ tests/server/test_tool_annotations.py (content)
⚔️ tests/server/test_tool_transformation.py (content)
⚔️ tests/tools/test_tool_timeout.py (content)
⚔️ tests/utilities/openapi/test_models.py (content)
⚔️ tests/utilities/openapi/test_schemas.py (content)
⚔️ tests/utilities/test_components.py (content)
⚔️ tests/utilities/test_inspect.py (content)
⚔️ tests/utilities/test_json_schema.py (content)
⚔️ uv.lock (content)
⚔️ v3-notes/get-methods-consolidation.md (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change—propagating origin_request_id to background task workers—which is the primary objective of this PR.
Description check ✅ Passed The pull request description includes all required template sections: clear description of changes, rationale, detailed change summary, and validation steps confirming testing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch fix/task-origin-request-id
  • Post resolved changes as copyable diffs in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/dependencies.py (1)

49-74: ⚠️ Potential issue | 🟡 Minor

OptionalCurrentContext missing from __all__.

CurrentContext is exported in __all__, but the new OptionalCurrentContext is not. If it's intended as a public API, add it for consistency and discoverability.

Proposed fix
 __all__ = [
     "AccessToken",
     "CurrentAccessToken",
     "CurrentContext",
     "CurrentDocket",
     "CurrentFastMCP",
     "CurrentHeaders",
     "CurrentRequest",
     "CurrentWorker",
+    "OptionalCurrentContext",
     "Progress",
🧹 Nitpick comments (2)
src/fastmcp/server/dependencies.py (1)

879-888: String-matching on exception message is fragile.

_OptionalCurrentContext.__aenter__ checks "No active context found" in str(exc) to distinguish "no context" from other RuntimeErrors. If the message in _CurrentContext.__aenter__ (line 862) is ever reworded, this silently breaks. Consider a dedicated exception subclass or a sentinel attribute instead.

♻️ Suggested approach
+class _NoActiveContextError(RuntimeError):
+    """Raised when no foreground or background context is available."""
+    pass

 class _CurrentContext(Dependency):
     ...
     async def __aenter__(self) -> Context:
         ...
         # Neither foreground nor background context available
-        raise RuntimeError(
+        raise _NoActiveContextError(
             "No active context found. This can happen if:\n"
             ...
         )

 class _OptionalCurrentContext(_CurrentContext):
     async def __aenter__(self) -> Context | None:
         try:
             return await super().__aenter__()
-        except RuntimeError as exc:
-            if "No active context found" in str(exc):
-                return None
-            raise
+        except _NoActiveContextError:
+            return None
src/fastmcp/__init__.py (1)

21-21: Consider whether IconTheme warrants top-level re-export.

IconTheme is a narrow Literal["light", "dark"] alias used only for icon theming. Per the project's coding guidelines, only the most fundamental types should be re-exported to fastmcp.*. Users can pass "light" / "dark" string literals directly without needing this type, and power users can import from fastmcp.utilities.types. That said, the docs already reference from fastmcp import IconTheme, so this may be intentional.

Based on learnings: "only re-export to fastmcp.* for the most fundamental types."

Also applies to: 35-35

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf308da05b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +105 to +106
origin_request_id = (
str(ctx.request_context.request_id) if ctx.request_context is not None else None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve origin request ID when submitting from task context

submit_to_docket snapshots lineage from ctx.request_context.request_id, but in background workers request_context is always None, so no origin_request_id key is written for child submissions. This breaks request correlation for task chains (a task enqueueing another task), because downstream workers cannot recover the original request ID and logs/status notifications lose lineage even though Context.origin_request_id is available in that context.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I think if a task calls another task, it should provide all the necessary info to it. I don't think we want to magically propagate that.

@gfortaine gfortaine changed the title Fix background task request lineage and optional Context injection continue #2905/#2906: close the last background reliability gap Feb 13, 2026
@gfortaine gfortaine force-pushed the fix/task-origin-request-id branch 3 times, most recently from 7d3c50d to 0d3f714 Compare February 13, 2026 02:45
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/fastmcp/server/tasks/handlers.py (1)

111-120: Consider using a Redis pipeline for the batch of SET operations.

There are now up to 5 individual redis.set calls inside the async with docket.redis() block. A pipeline would reduce round-trips. This is a pre-existing pattern so not blocking, but worth noting as the number of writes grows.

♻️ Suggested pipeline approach
     async with docket.redis() as redis:
-        await redis.set(task_meta_key, task_key, ex=ttl_seconds)
-        await redis.set(created_at_key, created_at.isoformat(), ex=ttl_seconds)
-        await redis.set(poll_interval_key, str(poll_interval_ms), ex=ttl_seconds)
-        if origin_request_id is not None:
-            await redis.set(origin_request_id_key, origin_request_id, ex=ttl_seconds)
-        if access_token is not None:
-            await redis.set(
-                access_token_key, access_token.model_dump_json(), ex=ttl_seconds
-            )
+        async with redis.pipeline(transaction=False) as pipe:
+            pipe.set(task_meta_key, task_key, ex=ttl_seconds)
+            pipe.set(created_at_key, created_at.isoformat(), ex=ttl_seconds)
+            pipe.set(poll_interval_key, str(poll_interval_ms), ex=ttl_seconds)
+            if origin_request_id is not None:
+                pipe.set(origin_request_id_key, origin_request_id, ex=ttl_seconds)
+            if access_token is not None:
+                pipe.set(
+                    access_token_key, access_token.model_dump_json(), ex=ttl_seconds
+                )
+            await pipe.execute()
src/fastmcp/server/context.py (1)

384-390: report_progress still uses self.request_id — safe today but inconsistent with log().

In background tasks, request_context is None, so progress_token is None and the method returns early at Line 382 before reaching self.request_id. However, for consistency with the log() fix, consider updating this to self.origin_request_id as well to prevent future breakage if the early-return logic changes.

♻️ Consistency fix
         await self.session.send_progress_notification(
             progress_token=progress_token,
             progress=progress,
             total=total,
             message=message,
-            related_request_id=self.request_id,
+            related_request_id=self.origin_request_id,
         )
src/fastmcp/server/dependencies.py (2)

823-864: get_task_session may return None if the session was garbage-collected — background task context will be created with no usable session.

At Line 835, get_task_session(task_info.session_id) can return None if the submitting client disconnected. The Context is still created (Line 842-847) and entered, but any subsequent call to ctx.session (e.g., from ctx.log()) will raise RuntimeError("session is not available...").

This is arguably expected behavior (can't send messages to a disconnected client), but a proactive warning log when session is None would help with debugging.

♻️ Add a warning when session is gone
             session = get_task_session(task_info.session_id)
+            if session is None:
+                _logger.warning(
+                    "Session %s no longer available for background task %s; "
+                    "context operations requiring a session will fail.",
+                    task_info.session_id,
+                    task_info.task_id,
+                )
             # Get server from ContextVar
             server = get_server()

877-886: String-based exception matching is fragile.

Line 884 checks "No active context found" in str(exc), which is coupled to the exact error message text at Line 860. If that message is ever rephrased, this catch silently stops working and None won't be returned — instead, the RuntimeError will propagate.

Consider using a sentinel or a dedicated exception subclass for more robust matching.

♻️ Example: use a custom exception
+class _NoActiveContextError(RuntimeError):
+    """Raised when no foreground or background context is available."""
+    pass
+

 class _CurrentContext(Dependency):
     ...
     async def __aenter__(self) -> Context:
         ...
         # Neither foreground nor background context available
-        raise RuntimeError(
+        raise _NoActiveContextError(
             "No active context found. This can happen if:\n"
             ...
         )

 class _OptionalCurrentContext(_CurrentContext):
     async def __aenter__(self) -> Context | None:
         try:
             return await super().__aenter__()
-        except RuntimeError as exc:
-            if "No active context found" in str(exc):
-                return None
-            raise
+        except _NoActiveContextError:
+            return None

Comment on lines 49 to 74
__all__ = [
"AccessToken",
"CurrentAccessToken",
"CurrentContext",
"CurrentDocket",
"CurrentFastMCP",
"CurrentHeaders",
"CurrentRequest",
"CurrentWorker",
"Progress",
"TaskContextInfo",
"TokenClaim",
"get_access_token",
"get_context",
"get_http_headers",
"get_http_request",
"get_server",
"get_task_context",
"get_task_session",
"is_docket_available",
"register_task_session",
"require_docket",
"resolve_dependencies",
"transform_context_annotations",
"without_injected_parameters",
]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

__all__ exports four undefined names — from fastmcp.server.dependencies import * will raise AttributeError.

Static analysis (Ruff F822) confirms that CurrentAccessToken, CurrentHeaders, CurrentRequest, and TokenClaim are not defined or imported in this file, yet they appear in __all__. This will break wildcard imports and mislead IDE autocompletion.

🐛 Proposed fix: remove undefined names
 __all__ = [
     "AccessToken",
-    "CurrentAccessToken",
     "CurrentContext",
     "CurrentDocket",
     "CurrentFastMCP",
-    "CurrentHeaders",
-    "CurrentRequest",
     "CurrentWorker",
+    "OptionalCurrentContext",
     "Progress",
     "TaskContextInfo",
-    "TokenClaim",
     "get_access_token",
     "get_context",
     "get_http_headers",
     "get_http_request",
     "get_server",
     "get_task_context",
     "get_task_session",
     "is_docket_available",
     "register_task_session",
     "require_docket",
     "resolve_dependencies",
     "transform_context_annotations",
     "without_injected_parameters",
 ]

Also note that OptionalCurrentContext (defined at Line 914) is missing from __all__ despite being a new public API.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
__all__ = [
"AccessToken",
"CurrentAccessToken",
"CurrentContext",
"CurrentDocket",
"CurrentFastMCP",
"CurrentHeaders",
"CurrentRequest",
"CurrentWorker",
"Progress",
"TaskContextInfo",
"TokenClaim",
"get_access_token",
"get_context",
"get_http_headers",
"get_http_request",
"get_server",
"get_task_context",
"get_task_session",
"is_docket_available",
"register_task_session",
"require_docket",
"resolve_dependencies",
"transform_context_annotations",
"without_injected_parameters",
]
__all__ = [
"AccessToken",
"CurrentContext",
"CurrentDocket",
"CurrentFastMCP",
"CurrentWorker",
"OptionalCurrentContext",
"Progress",
"TaskContextInfo",
"get_access_token",
"get_context",
"get_http_headers",
"get_http_request",
"get_server",
"get_task_context",
"get_task_session",
"is_docket_available",
"register_task_session",
"require_docket",
"resolve_dependencies",
"transform_context_annotations",
"without_injected_parameters",
]
🧰 Tools
🪛 Ruff (0.15.0)

[error] 51-51: Undefined name CurrentAccessToken in __all__

(F822)


[error] 55-55: Undefined name CurrentHeaders in __all__

(F822)


[error] 56-56: Undefined name CurrentRequest in __all__

(F822)


[error] 60-60: Undefined name TokenClaim in __all__

(F822)

@gfortaine gfortaine changed the title continue #2905/#2906: close the last background reliability gap fix: propagate origin_request_id to background task workers Feb 13, 2026
@gfortaine gfortaine force-pushed the fix/task-origin-request-id branch from 0d3f714 to c0956a2 Compare February 13, 2026 12:03
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The static analysis workflow failed due to formatting issues and an unused type ignore comment.

Root Cause: The PR introduced a new class _OptionalCurrentContext with a # type: ignore[misc] comment (line 877 in src/fastmcp/server/dependencies.py), but the type checker no longer needs this suppression — meaning the type error that was being suppressed has been resolved. Additionally, two files need reformatting to match the project's code style.

Suggested Solution:

  1. Remove the unused # type: ignore[misc] comment from line 877 (the _OptionalCurrentContext class definition)
  2. Run uv run prek run --all-files locally to auto-format the two files

The specific changes needed:

File: src/fastmcp/server/dependencies.py

# Change this line (line 877):
class _OptionalCurrentContext(_CurrentContext):  # type: ignore[misc]

# To this:
class _OptionalCurrentContext(_CurrentContext):

Then run uv run prek run --all-files to apply formatting fixes automatically.

Detailed Analysis

Type Checker Failure

warning[unused-type-ignore-comment]: Unused blanket `type: ignore` directive
     --> src/fastmcp/server/dependencies.py:877:50
      |
  877 | class _OptionalCurrentContext(_CurrentContext):  # type: ignore[misc]
      |                                                  ^^^^^^^^^^^^^^^^^^^^
  878 |     """Context dependency that degrades to None when no context is active."""
      |
  help: Remove the unused suppression comment

The type checker is warning that the # type: ignore[misc] comment is no longer needed. This typically happens when:

  • The original type error has been fixed elsewhere
  • The type checker has improved and can now infer the types correctly
  • The parent class _CurrentContext already has the same suppression (line 775)

Since _CurrentContext (the parent class) already has # type: ignore[misc], and _OptionalCurrentContext only overrides __aenter__ with a compatible signature, the suppression is redundant.

Ruff Format Failure

ruff format..............................................................Failed
- hook id: ruff-format
- files were modified by this hook

  2 files reformatted, 620 files left unchanged

Two files need formatting:

  1. src/fastmcp/server/dependencies.py - The new _restore_task_origin_request_id function signature needs to be on one line
  2. tests/server/test_dependencies.py - The tool_with_optional_context function signature needs to be split across multiple lines

Running prek will automatically fix both.

Related Files
  • src/fastmcp/server/dependencies.py (line 877): Contains the _OptionalCurrentContext class with the unused type ignore comment, and formatting issues in the new _restore_task_origin_request_id function
  • tests/server/test_dependencies.py: Contains formatting issues in test function signatures
  • .pre-commit-config.yaml: Defines the prek hooks that enforce these checks

@gfortaine gfortaine force-pushed the fix/task-origin-request-id branch 2 times, most recently from 255a2d9 to 23a1918 Compare February 13, 2026 12:32
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The static_analysis CI job failed due to a ty type-checking error: a subclass overrides __aenter__ with a wider return type (Context | None) than the parent class declares (Context), violating the Liskov Substitution Principle.

Root Cause: In src/fastmcp/server/dependencies.py at line 888, the PR introduces a subclass of _CurrentContext with async def __aenter__(self) -> Context | None:. The parent class _CurrentContext.__aenter__ (line 795 in main) declares return type Context (non-optional). ty correctly flags this as invalid-method-override since subclasses must not widen return types.

```
error[invalid-method-override]: Invalid override of method __aenter__
--> src/fastmcp/server/dependencies.py:888:15
```

Suggested Solution: The subclass at line 888 should not inherit from _CurrentContext if it needs to return Context | None. Two options:

  1. Create an independent class (preferred): Rather than subclassing _CurrentContext, implement a standalone class that handles the optional-context logic directly — returning None instead of raising when no context is available.

  2. Widen the parent's return type: Change _CurrentContext.__aenter__ to return Context | None and update CurrentContext() and all call sites to handle the None case. This is more invasive.

Option 1 avoids breaking existing callers of CurrentContext() and is the minimal fix.

Detailed Analysis

The prek static analysis step failed with exit code 1. Only the ty type checker raised an error — ruff, prettier, codespell, and pyproject.toml validation all passed.

Full ty error:
```
error[invalid-method-override]: Invalid override of method __aenter__
--> src/fastmcp/server/dependencies.py:888:15
|
886| """Context dependency that degrades to None when no context is active."""
887|
888| async def aenter(self) -> Context | None:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with _CurrentContext.__aenter__
889| try:
890| return await super().aenter()
|
::: src/fastmcp/server/dependencies.py:831:15
|
829| _access_token_cv_token: Token[AccessToken | None] | None = None
830|
831| async def aenter(self) -> Context:
| --------------------------- _CurrentContext.__aenter__ defined here
|
info: This violates the Liskov Substitution Principle
info: rule invalid-method-override is enabled by default

Found 1 diagnostic
```

The parent class _CurrentContext always returns Context or raises (never returns None). The new subclass catches the RuntimeError and returns None instead — which is fine functionally, but ty enforces that a subclass's return type must be ≤ the parent's (covariant), not ≥.

Note: loq file-size violations were also reported for 13 files, but are marked as "not enforced yet" and did not contribute to the failure.

Related Files
  • src/fastmcp/server/dependencies.py — contains _CurrentContext (parent, line 784) and the new subclass (~line 882-895 in PR branch). This is the file that needs to be changed.

@gfortaine
Copy link
Copy Markdown
Contributor Author

gfortaine commented Feb 20, 2026

@jlowin @chrisguidry

first: congrats on the 3.0 ga push — two betas, two rcs, 21 new contributors, ~100k opt-in prerelease installs, upgrade guides, and the move to prefecthq… that’s a seriously demanding week.

quick ask: could you take a look at this pr when you have a moment? it unblocks full-featured task mode in the wild (my gemini-research-mcp server can’t run correctly without it because background work loses request lineage).

i’m aiming to get this in 3.0.2 if it fits the release bar. what would you need to see (or what would you want changed) to feel good about merging / backporting it?

happy to reshape/split it to match your preference and keep risk low.

Copy link
Copy Markdown
Collaborator

@chrisguidry chrisguidry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to me! Thanks!!

@gfortaine gfortaine force-pushed the fix/task-origin-request-id branch from de5dc39 to 8f53d7d Compare February 21, 2026 00:11
Refactor OptionalCurrentContext to wrap CurrentContext instead of overriding __aenter__ with a wider return type. Adds a background-task origin_request_id round-trip test and applies ruff formatting.
@gfortaine gfortaine force-pushed the fix/task-origin-request-id branch from 8f53d7d to 839f0d0 Compare February 21, 2026 15:44
@jlowin
Copy link
Copy Markdown
Member

jlowin commented Feb 22, 2026

Thanks @gfortaine, let's get this merged!

@jlowin jlowin merged commit 40d3190 into PrefectHQ:main Feb 22, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality. tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Passing context to Background Task

3 participants