Repository navigation
fix(mcp): add safeguards to ensure all MCP tools are wrapped with mcp_auth_hook #39395
Description
Context
PR #39385 fixed a critical race condition where concurrent MCP tool calls could overwrite each other's g.user, causing user impersonation. The fix (pushing a fresh app_context() per tool call) is correct, but its safety depends entirely on mcp_auth_hook wrapping every registered tool. There are currently three bypass paths that could silently produce unprotected tools.
Problem
-
protect=Falseopt-out — Any tool decorated with@tool(protect=False)skipsmcp_auth_hookentirely. If such a tool touchesg.userordb.session, it's vulnerable to the race condition. -
Silent fallback in
create_tool_decorator— Whenmcp_auth_hookraises during decoration (e.g., import error), the tool is returned unwrapped with only alogger.error(). The tool is then served without auth or context isolation, silently. -
Direct
mcp.add_tool()bypass — Code that importsmcpdirectly and callsmcp.add_tool()(currently only used for resources, but nothing prevents tool registration this way) bypasses the decorator entirely.
Proposed Fix
1. Stamp a protection marker in mcp_auth_hook (~1 line)
# In auth.py, at the end of mcp_auth_hook():
new_wrapper._mcp_auth_protected = True2. Add a startup assertion (~15 lines)
After all tools are registered (in init_fastmcp_server() or __main__.py), iterate the FastMCP tool registry and verify every tool has the marker:
ALLOWED_UNPROTECTED = {"health_check"}
def assert_all_tools_protected(mcp_instance):
for name, tool in mcp_instance._tool_manager._tools.items():
if name in ALLOWED_UNPROTECTED:
continue
if not getattr(tool.fn, '_mcp_auth_protected', False):
raise RuntimeError(
f"SECURITY: Tool '{name}' registered without mcp_auth_hook. "
f"All tools must use @tool() with protect=True or be explicitly allowlisted."
)This fails loudly at server startup — catches all three bypass paths before any request is served.
3. Fail-fast in create_tool_decorator (~3 lines)
Change the exception handler in create_tool_decorator from silently returning the unwrapped function to either raising or registering a no-op tool that always errors:
except Exception as e:
logger.error("Failed to register MCP tool %s: %s", name or func.__name__, e)
raise # Fail fast — don't silently serve unprotected tools4. CI lint (optional, defense-in-depth)
A grep-based check that flags protect=False and direct mcp.add_tool() in tool/ directories.
Why This Matters
The g.user race condition fix in #39385 is correct but structurally fragile — it relies on a decorator being applied to every tool. These safeguards make that invariant enforceable rather than convention-based, turning a silent security regression into a loud startup failure.
Effort
~20 lines of code, plus tests. Small PR.
Activity
jaymasiwal commentedon Apr 18, 2026
Hey @mistercrunch, I'd love to take this one on! Ensuring the mcp_auth_hook invariant is structurally enforced is a great safeguard.
I'll implement the proposed fail-fast logic in create_tool_decorator, stamp the wrapper, and add the startup assertion to catch any bypasses (along with the necessary unit tests). I'll get a PR up shortly!
Abdulrehman-PIAIC80387 commentedon May 25, 2026
Hi @jaymasiwal — are you still working on this? If not, I have a fix ready and will open a PR shortly. Thanks!