PrefectHQ/fastmcp · error · ValueError

Invalid message role: {message.role}

Error message

Invalid message role: {message.role}

What it means

_convert_messages_to_google_genai_content raises ValueError when a sampling message has a role other than 'user' or 'assistant'. Google GenAI Content only defines user/model roles, so any other role (e.g. a future or malformed MCP role) cannot be mapped.

Source

Thrown at fastmcp_slim/fastmcp/client/sampling/handlers/google_genai.py:297

    """Convert MCP messages to Google GenAI content."""
    google_messages: list[Content] = []

    for message in messages:
        content = message.content

        # Handle list content (tool calls + results)
        if isinstance(content, list):
            parts: list[Part] = [
                _sampling_content_to_google_genai_part(item) for item in content
            ]

            if message.role == "user":
                google_messages.append(UserContent(parts=parts))
            elif message.role == "assistant":
                google_messages.append(ModelContent(parts=parts))
            else:
                msg = f"Invalid message role: {message.role}"
                raise ValueError(msg)
            continue

        # Handle single content item
        part = _sampling_content_to_google_genai_part(content)

        if message.role == "user":
            google_messages.append(UserContent(parts=[part]))
        elif message.role == "assistant":
            google_messages.append(ModelContent(parts=[part]))
        else:
            msg = f"Invalid message role: {message.role}"
            raise ValueError(msg)

    return google_messages


def _get_candidate_from_response(response: GenerateContentResponse) -> Candidate:
    """Extract the first candidate from a response."""

View on GitHub (pinned to 1f02114297)

Solutions

  1. Use only 'user' or 'assistant' roles in sampling messages; move system text to SamplingParams.systemPrompt.
  2. Fix the server side if it emits non-standard roles.
  3. Catch ValueError and remap unknown roles to 'user' in your sampling callback.
  4. Upgrade fastmcp-slim if a new MCP role needs official mapping.

Example fix

// before
SamplingMessage(role="system", content=TextContent(type="text", text="Be concise"))
// after
SamplingMessage(role="user", content=TextContent(type="text", text="Be concise"))
# or set params.systemPrompt = "Be concise"
Defensive patterns

Strategy: validation

Validate before calling

def validate_roles(messages):
    for m in messages:
        if m.role not in ("user", "assistant"):
            raise ValueError(f"Invalid role for Gemini handler: {m.role!r}")

Type guard

def has_valid_role(m) -> bool:
    return getattr(m, "role", None) in ("user", "assistant")

Try / catch

try:
    result = await handler(messages, params, context)
except ValueError as e:
    if "Invalid message role" in str(e):
        messages = [m if m.role in ("user", "assistant") else m.model_copy(update={"role": "user"}) for m in messages]
        result = await handler(messages, params, context)
    else:
        raise

Prevention

When it happens

Trigger: A sampling request message has role set to something other than the MCP-standard 'user' or 'assistant' — e.g. hand-built SamplingMessage with a typo like 'system' or 'tool'.

Common situations: Constructing SamplingMessage manually with 'system' role (system prompts belong in params.systemPrompt, not messages); copying roles from OpenAI-style payloads; MCP spec changes.

Related errors


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