Offload sync function calls to a thread#3010
Open
Rifa-111 wants to merge 1 commit into
Open
Conversation
Offload synchronous function calls to a thread to prevent blocking the event loop. This change ensures that context is correctly propagated using anyio's run_sync.
There was a problem hiding this comment.
1 issue found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/mcp/server/fastmcp/utilities/func_metadata.py">
<violation number="1" location="src/mcp/server/fastmcp/utilities/func_metadata.py:96">
P1: Add the missing `anyio` import before using `anyio.to_thread.run_sync`; otherwise every synchronous tool call fails at runtime.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| return await fn(**arguments_parsed_dict) | ||
| else: | ||
| return fn(**arguments_parsed_dict) | ||
| return await anyio.to_thread.run_sync(lambda: fn(**arguments_parsed_dict)) |
There was a problem hiding this comment.
P1: Add the missing anyio import before using anyio.to_thread.run_sync; otherwise every synchronous tool call fails at runtime.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/server/fastmcp/utilities/func_metadata.py, line 96:
<comment>Add the missing `anyio` import before using `anyio.to_thread.run_sync`; otherwise every synchronous tool call fails at runtime.</comment>
<file context>
@@ -93,7 +93,11 @@ async def call_fn_with_arg_validation(
return await fn(**arguments_parsed_dict)
else:
- return fn(**arguments_parsed_dict)
+ return await anyio.to_thread.run_sync(lambda: fn(**arguments_parsed_dict))
+ # Sync functions are offloaded to a thread to avoid blocking the event loop.
+ # anyio.to_thread.run_sync() uses copy_context() internally, so contextvars
</file context>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Sync tool functions registered with FastMCP were called directly on the
asyncio event loop, blocking it for the duration of the call. Any sync
tool performing I/O (e.g. using
requests,time.sleep, file reads)would stall all other concurrent tasks on the same server.
Change
In
call_fn_with_arg_validation(src/mcp/server/fastmcp/utilities/func_metadata.py),wrap sync calls with
anyio.to_thread.run_sync()instead of callingthem directly:
Before:
After:
This is consistent with how FastAPI handles sync route handlers, and how
FileResourceandDirectoryResourcealready handle blocking readswithin this codebase.
anyio.to_thread.run_sync()usescopy_context()internally socontextvars propagate correctly to the worker thread. The MCP
Contextobject is unaffected as it is passed explicitly via
arguments_to_pass_directly.Testing
Added a test that registers a sync tool using
time.sleepand assertsthat two concurrent calls complete in roughly the time of one sleep,
confirming they run in parallel without blocking the event loop.
Closes #1839