Let FastMCPError propagate from dependencies

When a dependency raises ToolError or other FastMCPError subclasses, they
were getting wrapped in RuntimeError with a generic "Failed to resolve
dependency" message. This made it hard to use ToolError for validation
in dependencies.

Now FastMCPError subclasses propagate unchanged, matching the pattern
used elsewhere in the codebase.

Closes #2633

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris Guidry 2025-12-18 10:59:24 -05:00
commit 63bc9cb921
2 changed files with 49 additions and 0 deletions

View file

@ -21,6 +21,7 @@ from mcp.server.auth.provider import (
from mcp.server.lowlevel.server import request_ctx
from starlette.requests import Request
from fastmcp.exceptions import FastMCPError
from fastmcp.server.auth import AccessToken
from fastmcp.server.http import _current_http_request
from fastmcp.utilities.types import is_class_member_of_type
@ -188,6 +189,10 @@ async def _resolve_fastmcp_dependencies(
resolved[parameter] = await stack.enter_async_context(
dependency
)
except FastMCPError:
# Let FastMCPError subclasses (ToolError, ResourceError, etc.)
# propagate unchanged so they can be handled appropriately
raise
except Exception as error:
fn_name = getattr(fn, "__name__", repr(fn))
raise RuntimeError(

View file

@ -733,3 +733,47 @@ async def test_resource_template_uri_cannot_match_dependency_name(mcp: FastMCP):
@mcp.resource("auth://{token}/validate")
async def validate(token: str = Depends(get_token)) -> str:
return f"Validating with: {token}"
async def test_toolerror_propagates_from_dependency(mcp: FastMCP):
"""ToolError raised in a dependency should propagate unchanged (issue #2633).
When a dependency raises ToolError, it should not be wrapped in RuntimeError.
This allows developers to use ToolError for validation in dependencies.
"""
from fastmcp.exceptions import ToolError
def validate_client_id() -> str:
raise ToolError("Client ID is required - select a client first")
@mcp.tool()
async def my_tool(client_id: str = Depends(validate_client_id)) -> str:
return f"Working with client: {client_id}"
async with Client(mcp) as client:
# ToolError is converted to an error result by the server
result = await client.call_tool("my_tool", {}, raise_on_error=False)
assert result.is_error
# The original error message should be preserved (not wrapped in RuntimeError)
assert result.content[0].text == "Client ID is required - select a client first" # type: ignore[attr-defined]
async def test_validation_error_propagates_from_dependency(mcp: FastMCP):
"""ValidationError raised in a dependency should propagate unchanged."""
from fastmcp.exceptions import ValidationError
def validate_input() -> str:
raise ValidationError("Invalid input format")
@mcp.tool()
async def tool_with_validation(val: str = Depends(validate_input)) -> str:
return val
async with Client(mcp) as client:
# ValidationError is re-raised by the server and becomes an error result
# The original error message should be preserved (not wrapped in RuntimeError)
result = await client.call_tool(
"tool_with_validation", {}, raise_on_error=False
)
assert result.is_error
assert result.content[0].text == "Invalid input format" # type: ignore[attr-defined]