diff --git a/src/fastmcp/server/dependencies.py b/src/fastmcp/server/dependencies.py index 31b170c79f..3671ba42b8 100644 --- a/src/fastmcp/server/dependencies.py +++ b/src/fastmcp/server/dependencies.py @@ -21,6 +21,7 @@ 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( diff --git a/tests/server/test_dependencies.py b/tests/server/test_dependencies.py index 208ae31066..a05d3d4366 100644 --- a/tests/server/test_dependencies.py +++ b/tests/server/test_dependencies.py @@ -733,3 +733,47 @@ def get_token() -> str: @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]