From 63bc9cb921237a134e1919b82953567d2d2c93e6 Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Thu, 18 Dec 2025 10:59:24 -0500 Subject: [PATCH] Let FastMCPError propagate from dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/fastmcp/server/dependencies.py | 5 ++++ tests/server/test_dependencies.py | 44 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/fastmcp/server/dependencies.py b/src/fastmcp/server/dependencies.py index 31b170c79..3671ba42b 100644 --- a/src/fastmcp/server/dependencies.py +++ b/src/fastmcp/server/dependencies.py @@ -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( diff --git a/tests/server/test_dependencies.py b/tests/server/test_dependencies.py index 208ae3106..a05d3d436 100644 --- a/tests/server/test_dependencies.py +++ b/tests/server/test_dependencies.py @@ -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]