PrefectHQ/fastmcp · error · ValueError

Unsupported tool_choice mode: {tool_choice.mode!r}

Error message

Unsupported tool_choice mode: {tool_choice.mode!r}

What it means

_convert_tool_choice_to_anthropic raises ValueError for a ToolChoice mode it does not recognize. Anthropic supports auto/any (required)/none mappings; anything else (e.g. a new MCP mode) is rejected rather than silently mis-mapped.

Source

Thrown at fastmcp_slim/fastmcp/client/sampling/handlers/anthropic.py:404

    @staticmethod
    def _convert_tool_choice_to_anthropic(
        tool_choice: ToolChoice,
    ) -> ToolChoiceParam | None:
        """Convert MCP tool_choice to Anthropic format.

        Returns None for "none" mode, signaling that tools should be omitted
        from the request entirely (Anthropic doesn't have an explicit "none" option).
        """
        if tool_choice.mode == "auto":
            return ToolChoiceAutoParam(type="auto")
        elif tool_choice.mode == "required":
            return ToolChoiceAnyParam(type="any")
        elif tool_choice.mode == "none":
            # Anthropic doesn't have a "none" option - return None to signal
            # that tools should be omitted from the request entirely
            return None
        else:
            raise ValueError(f"Unsupported tool_choice mode: {tool_choice.mode!r}")

    @staticmethod
    def _message_to_result_with_tools(
        message: Message,
    ) -> CreateMessageResultWithTools:
        """Convert Anthropic response to CreateMessageResultWithTools."""
        if len(message.content) == 0:
            raise ValueError("No content in response from Anthropic")

        # Determine stop reason
        stop_reason: StopReason
        if message.stop_reason == "tool_use":
            stop_reason = "toolUse"
        elif message.stop_reason == "end_turn":
            stop_reason = "endTurn"
        elif message.stop_reason == "max_tokens":
            stop_reason = "maxTokens"
        elif message.stop_reason == "stop_sequence":

View on GitHub (pinned to 1f02114297)

Solutions

  1. Set tool_choice.mode to 'auto', 'required', or 'none' before calling the handler.
  2. Upgrade fastmcp-slim in case a newer MCP mode has gained support.
  3. Catch ValueError and default tool_choice to auto in your sampling callback.
  4. Check the server implementation producing the sampling request for non-standard modes.

Example fix

// before
params.tool_choice = ToolChoice(mode="specific")
// after
params.tool_choice = ToolChoice(mode="auto")  # or 'required' / 'none'
Defensive patterns

Strategy: validation

Validate before calling

VALID_MODES = {"auto", "required", "none"}
if params.tool_choice and params.tool_choice.mode not in VALID_MODES:
    params.tool_choice = None  # or ToolChoice(mode="auto")

Type guard

def is_valid_tool_choice(tc) -> bool:
    return tc is None or getattr(tc, "mode", None) in {"auto", "required", "none"}

Try / catch

try:
    result = await handler(messages, params, context)
except ValueError as e:
    if "Unsupported tool_choice mode" in str(e):
        params.tool_choice = ToolChoice(mode="auto")
        result = await handler(messages, params, context)
    else:
        raise

Prevention

When it happens

Trigger: A server sends a sampling request with tool_choice.mode set to a value other than 'auto', 'required', or 'none' (or a newer MCP spec mode the handler predates).

Common situations: MCP spec updates introducing new tool_choice modes; hand-constructed SamplingParams with a typo'd mode string; forwarding a tool_choice from another provider unmodified.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/864799a213248611. Report an issue: GitHub.