Skip to content

Run sync tools/resources/prompts in threadpool automatically#2865

Merged
jlowin merged 2 commits intomainfrom
sync-threadpool-execution
Jan 13, 2026
Merged

Run sync tools/resources/prompts in threadpool automatically#2865
jlowin merged 2 commits intomainfrom
sync-threadpool-execution

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Jan 13, 2026

Sync functions for tools, resources, and prompts now automatically run in a threadpool, preventing them from blocking the event loop during concurrent requests.

@mcp.tool
def slow_tool():
    time.sleep(10)  # No longer blocks other requests
    return "done"

Previously, 3 concurrent calls to this tool would take 30 seconds (sequential). Now they take ~10 seconds (parallel).

Uses anyio.to_thread.run_sync() which properly propagates contextvars, so Context and Depends continue to work. This matches FastAPI/Starlette's behavior for sync endpoints.

Also fixes callable object support for resources (was missing the __call__ normalization that tools/prompts had).

Sync functions now automatically run in a threadpool via anyio.to_thread.run_sync(),
preventing them from blocking the event loop during concurrent requests. This matches
FastAPI/Starlette's behavior for sync endpoints.

Also fixes callable object support for resources (normalization and name extraction).
@marvin-context-protocol marvin-context-protocol Bot added enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality. labels Jan 13, 2026
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: 112e1e6d02

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/fastmcp/server/dependencies.py Outdated
Comment on lines +480 to +484
if fn_is_async:
return await fn(**resolved_kwargs)
else:
# Run sync functions in threadpool to avoid blocking the event loop
return await call_sync_fn_in_threadpool(fn, **resolved_kwargs)
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 Await awaitables returned from sync wrappers

The new wrapper only awaits when inspect.iscoroutinefunction(fn) is true; otherwise it runs fn in a threadpool and returns whatever it produces. If a sync wrapper returns an awaitable (e.g., a functools.partial(async_fn, ...), or a decorator that hides coroutinefunction), this path now returns a coroutine object instead of awaiting it. Previously, any awaitable result was awaited, so these callables worked. With the change, prompts/resources/tools will try to serialize a coroutine or behave incorrectly. Consider awaiting result when it’s inspect.isawaitable(result) after the threadpool call to preserve the prior behavior for such wrappers.

Useful? React with 👍 / 👎.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 13, 2026

Walkthrough

This pull request adds a new async helper call_sync_fn_in_threadpool and integrates it so synchronous callables are executed in a threadpool rather than directly on the event loop. The change updates runtime paths in prompt rendering, resource reading, tool execution, and dependency resolution to detect coroutine vs sync functions and offload sync functions to the threadpool (awaiting results and any returned awaitables as needed). Documentation pages for prompts, resources, and tools are updated to reflect support for both sync and async implementations.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and concisely summarizes the main change: synchronous functions for tools, resources, and prompts now run in a threadpool automatically.
Description check ✅ Passed The description provides clear context for the changes with concrete examples and rationale, but lacks the completed contributors/review checklists required by the template.

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

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 112e1e6 and c8bb0b1.

📒 Files selected for processing (4)
  • src/fastmcp/prompts/function_prompt.py
  • src/fastmcp/resources/function_resource.py
  • src/fastmcp/server/dependencies.py
  • src/fastmcp/tools/function_tool.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Python ≥3.10 with full type annotations required for all code
Never use bare except - be specific with exception types in Python code

Files:

  • src/fastmcp/tools/function_tool.py
  • src/fastmcp/prompts/function_prompt.py
  • src/fastmcp/server/dependencies.py
  • src/fastmcp/resources/function_resource.py
🧠 Learnings (1)
📚 Learning: 2026-01-13T03:11:40.907Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-13T03:11:40.907Z
Learning: Applies to **/__init__.py : Be intentional about module re-exports - only re-export fundamental types to fastmcp.*; prefer users importing from specific submodules

Applied to files:

  • src/fastmcp/server/dependencies.py
🧬 Code graph analysis (4)
src/fastmcp/tools/function_tool.py (3)
src/fastmcp/server/context.py (1)
  • fastmcp (186-191)
src/fastmcp/server/dependencies.py (1)
  • without_injected_parameters (434-499)
src/fastmcp/utilities/async_utils.py (1)
  • call_sync_fn_in_threadpool (13-21)
src/fastmcp/prompts/function_prompt.py (1)
src/fastmcp/utilities/async_utils.py (1)
  • call_sync_fn_in_threadpool (13-21)
src/fastmcp/server/dependencies.py (1)
src/fastmcp/utilities/async_utils.py (1)
  • call_sync_fn_in_threadpool (13-21)
src/fastmcp/resources/function_resource.py (1)
src/fastmcp/utilities/async_utils.py (1)
  • call_sync_fn_in_threadpool (13-21)
⏰ 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). (6)
  • GitHub Check: Analyze (actions)
  • GitHub Check: Analyze (python)
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on windows-latest
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (10)
src/fastmcp/prompts/function_prompt.py (2)

33-33: LGTM!

Import added correctly and is consistent with other files in this PR.


297-304: LGTM! Correct pattern for sync/async handling.

The implementation properly:

  1. Detects coroutine functions and awaits directly
  2. Offloads sync functions to threadpool to avoid blocking
  3. Handles edge case where sync wrappers return awaitables (e.g., functools.partial(async_fn))

This is consistent with the same pattern applied in dependencies.py, function_tool.py, and function_resource.py.

src/fastmcp/server/dependencies.py (2)

32-32: LGTM!

Import added correctly.


475-487: LGTM! Efficient and correct implementation.

Good design choices:

  1. fn_is_async is captured once at wrapper creation time (line 475), avoiding repeated introspection on each call
  2. Async functions are awaited directly without threadpool overhead
  3. Sync functions run in threadpool to avoid blocking the event loop
  4. The isawaitable check handles edge cases like functools.partial(async_fn)

The pattern is consistent with the other files in this PR.

src/fastmcp/tools/function_tool.py (3)

24-24: LGTM!

Import consolidation is clean.


33-33: LGTM!

Import added correctly and consistent with other files.


239-250: LGTM! Correct handling of sync/async wrapper functions.

The logic correctly handles both cases:

  1. When wrapper_fn has dependencies, without_injected_parameters returns an async wrapper, so the async branch is taken
  2. When wrapper_fn has no dependencies, the original function is returned, and the sync branch runs it in a threadpool

The isawaitable check handles edge cases where the sync path might still return an awaitable.

src/fastmcp/resources/function_resource.py (3)

23-23: LGTM!

Import added correctly and consistent with other files.


150-170: LGTM! Consistent callable handling with function_prompt.py.

Good additions:

  1. Name resolution chain (metadata.name__name____class__.__name__) properly handles callable objects
  2. Callable class handling (line 166-167) and staticmethod unwrapping (line 169-170) match the pattern in function_prompt.py

This ensures resources work with callable objects, not just regular functions.


199-206: LGTM! Correct sync/async handling pattern.

The implementation:

  1. Awaits coroutine functions directly
  2. Offloads sync functions to threadpool to avoid blocking
  3. Handles sync wrappers returning awaitables

This is consistent with the same pattern in the other files in this PR.


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.

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)
docs/servers/prompts.mdx (1)

364-375: Add missing aiohttp import for runnable example.

The async example uses aiohttp.ClientSession() but the import statement is missing, making this code non-runnable. As per coding guidelines, documentation should include complete, runnable code examples.

📝 Suggested fix
 # Asynchronous prompt
 @mcp.prompt
 async def data_based_prompt(data_id: str) -> str:
     """Generates a prompt based on data that needs to be fetched."""
-    # In a real scenario, you might fetch data from a database or API
+    import aiohttp  # Or add to imports at top of your file
     async with aiohttp.ClientSession() as session:
         async with session.get(f"https://api.example.com/data/{data_id}") as response:
             data = await response.json()
             return f"Analyze this data: {data['content']}"

Alternatively, add import aiohttp alongside the other imports at line 31 in the initial code block, or use a comment indicating the required import.

🧹 Nitpick comments (1)
src/fastmcp/server/dependencies.py (1)

32-32: Consider whether re-exporting is intentional.

call_sync_fn_in_threadpool is already the canonical export from fastmcp.utilities.async_utils. Adding it to __all__ here creates a secondary import path (from fastmcp.server.dependencies import call_sync_fn_in_threadpool).

Based on learnings, the guidance is to "be intentional about module re-exports." If this re-export is for internal convenience within the server subpackage, that's fine. If it's meant as a public API, users should import from utilities.async_utils directly. Consider removing from __all__ if it's not intended as a public API of the dependencies module.

Also applies to: 50-50

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b6a0fa and 112e1e6.

⛔ Files ignored due to path filters (3)
  • tests/prompts/test_prompt.py is excluded by none and included by none
  • tests/resources/test_function_resources.py is excluded by none and included by none
  • tests/tools/test_tool.py is excluded by none and included by none
📒 Files selected for processing (8)
  • docs/servers/prompts.mdx
  • docs/servers/resources.mdx
  • docs/servers/tools.mdx
  • src/fastmcp/prompts/function_prompt.py
  • src/fastmcp/resources/function_resource.py
  • src/fastmcp/server/dependencies.py
  • src/fastmcp/tools/function_tool.py
  • src/fastmcp/utilities/async_utils.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Python ≥3.10 with full type annotations required for all code
Never use bare except - be specific with exception types in Python code

Files:

  • src/fastmcp/server/dependencies.py
  • src/fastmcp/resources/function_resource.py
  • src/fastmcp/tools/function_tool.py
  • src/fastmcp/utilities/async_utils.py
  • src/fastmcp/prompts/function_prompt.py
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/prompts.mdx
  • docs/servers/resources.mdx
  • docs/servers/tools.mdx
🧠 Learnings (5)
📚 Learning: 2026-01-13T03:11:40.907Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-13T03:11:40.907Z
Learning: Applies to **/__init__.py : Be intentional about module re-exports - only re-export fundamental types to fastmcp.*; prefer users importing from specific submodules

Applied to files:

  • src/fastmcp/server/dependencies.py
📚 Learning: 2026-01-12T16:24:54.978Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2026-01-12T16:24:54.978Z
Learning: Applies to src/prompts/**/*.{ts,tsx,js,jsx} : Changes affecting MCP Prompts (like adding tags, importing, etc.) must be adopted, applied, and tested consistently

Applied to files:

  • docs/servers/prompts.mdx
📚 Learning: 2026-01-12T16:24:54.978Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2026-01-12T16:24:54.978Z
Learning: Applies to src/resources/**/*.{ts,tsx,js,jsx} : Changes affecting MCP Resources (like adding tags, importing, etc.) must be adopted, applied, and tested consistently

Applied to files:

  • docs/servers/resources.mdx
📚 Learning: 2026-01-12T16:24:54.978Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2026-01-12T16:24:54.978Z
Learning: Applies to src/tools/**/*.{ts,tsx,js,jsx} : Changes affecting MCP Tools (like adding tags, importing, etc.) must be adopted, applied, and tested consistently

Applied to files:

  • docs/servers/tools.mdx
📚 Learning: 2025-11-03T17:36:13.363Z
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 2355
File: docs/clients/client.mdx:226-246
Timestamp: 2025-11-03T17:36:13.363Z
Learning: In FastMCP documentation, prefer showing the happy path in onboarding examples without over-explaining edge cases or adding defensive checks, as this reduces cognitive burden for new users learning the API.

Applied to files:

  • docs/servers/tools.mdx
🧬 Code graph analysis (4)
src/fastmcp/server/dependencies.py (1)
src/fastmcp/utilities/async_utils.py (1)
  • call_sync_fn_in_threadpool (13-21)
src/fastmcp/resources/function_resource.py (1)
src/fastmcp/utilities/async_utils.py (1)
  • call_sync_fn_in_threadpool (13-21)
src/fastmcp/tools/function_tool.py (2)
src/fastmcp/server/dependencies.py (1)
  • without_injected_parameters (435-496)
src/fastmcp/utilities/async_utils.py (1)
  • call_sync_fn_in_threadpool (13-21)
src/fastmcp/prompts/function_prompt.py (1)
src/fastmcp/utilities/async_utils.py (1)
  • call_sync_fn_in_threadpool (13-21)
⏰ 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 windows-latest
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (14)
src/fastmcp/utilities/async_utils.py (2)

3-8: LGTM on imports.

The imports are correctly organized, and the alias run_sync_in_threadpool provides clarity at the call site.


13-21: Implementation is correct.

The use of functools.partial to bundle fn with *args and **kwargs is the right approach since anyio.to_thread.run_sync expects a zero-argument callable. The contextvar propagation claim is accurate—anyio's run_sync copies contextvars from the calling task into the worker thread by default, making this safe for functions that depend on context (like dependency injection).

The return type is Any, which is acceptable for this utility pattern. A TypeVar could provide better type inference for callers if desired in the future.

src/fastmcp/resources/function_resource.py (4)

23-23: LGTM on import.


150-153: Good enhancement for callable object support.

The fallback chain (metadata.namefn.__name__fn.__class__.__name__) correctly handles callable classes that may not have __name__ but do have a class name.


165-170: Consistent callable normalization.

This matches the pattern used in FunctionPrompt.from_function and FunctionTool.from_function, ensuring uniform handling of callable classes and staticmethods across the codebase.


199-203: Sync branch reachability is correct and intentional.

The analysis is accurate: without_injected_parameters returns the original function unchanged when there are no Context or Depends() parameters to exclude (line 466 in dependencies.py). When dependencies exist, it returns an async wrapper that always handles both sync and async functions internally.

This design ensures:

  • Simple sync resources without DI use the threadpool branch (lines 200-201)
  • Resources with dependencies always use the async wrapper via the async branch (line 199)

The pattern is intentional and correct.

src/fastmcp/prompts/function_prompt.py (2)

33-33: LGTM on import.


297-301: Correct async/sync dispatch pattern.

This mirrors the pattern in FunctionResource.read(). The iscoroutinefunction check correctly identifies whether self.fn (from without_injected_parameters) is async or sync, routing sync prompts through the threadpool to avoid blocking.

src/fastmcp/server/dependencies.py (1)

476-484: Clean async/sync dispatch in wrapper.

Capturing fn_is_async = inspect.iscoroutinefunction(fn) outside the wrapper avoids repeated introspection on each call. The branching logic correctly routes sync functions through the threadpool while preserving the resolved kwargs from dependency injection.

src/fastmcp/tools/function_tool.py (2)

24-24: LGTM on imports.

Import reorganization and addition of call_sync_fn_in_threadpool are clean.

Also applies to: 33-33


239-247: Correct threadpool dispatch for sync tools.

The type_adapter.validate_python(arguments) call validates arguments against the function's schema and invokes the function. For sync wrapper_fn, this entire operation (validation + execution) runs in the threadpool, which is the correct behavior to avoid blocking.

Note: The comment "Async function: validate_python returns a coroutine" is slightly imprecise—validate_python itself is sync, but it returns an awaitable when the underlying function is async. The behavior is correct regardless.

docs/servers/tools.mdx (1)

143-158: Clear and practical documentation update.

The updated section correctly explains the new behavior: sync tools automatically run in a threadpool. The example with time.sleep(2) effectively demonstrates a blocking operation that won't block the event loop. The guidance to prefer async for I/O-bound work while allowing sync for simpler cases provides a good mental model for users.

docs/servers/resources.mdx (1)

273-292: Documentation aligns with implementation.

The updated section mirrors the tools documentation pattern: sync functions automatically run in a threadpool, async is more efficient for I/O. The aiofiles example demonstrates proper async file reading with appropriate error handling, providing a good template for users.

docs/servers/prompts.mdx (1)

355-362: LGTM!

The updated documentation clearly explains the new threadpool behavior for synchronous prompts. The comment on line 358 helpfully indicates that the sync function runs in a threadpool, which aligns with the PR's implementation changes.

Address PR review comments:
- Add inspect.isawaitable() check after threadpool execution to handle
  edge cases like functools.partial(async_fn)
- Remove call_sync_fn_in_threadpool from __all__ (internal utility)
@jlowin jlowin merged commit 8c471a4 into main Jan 13, 2026
11 checks passed
@jlowin jlowin deleted the sync-threadpool-execution branch January 13, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant