Skip to content

[Bug] GoogleGenaiSamplingHandler: thought parts leak as TextContent, unhelpful errors on empty/safety responses #3846

@strawgate

Description

@strawgate

Description

Three related bugs in GoogleGenaiSamplingHandler's response conversion functions (_response_to_result_with_tools and _response_to_create_message_result).

Bug 1: Thought parts leak through as TextContent

google_genai.py:363-365 has a comment saying "Skip thought parts" but the code only checks if part.text: — which is truthy for thought parts too.

# Current code (line 363-365)
# Note: Skip thought parts from thinking_config - not relevant for MCP responses
if part.text:
    content.append(TextContent(type="text", text=part.text))

When thinking_budget is set and Gemini returns thought parts, the internal reasoning text is converted to TextContent and appears as actual response content in the MCP message.

Fix: if part.text and not part.thought:

Bug 2: Thinking-only responses crash

When Gemini returns a response where ALL parts have thought=True (the model only thought but produced no output):

  • Non-tool path (_response_to_create_message_result): response.text returns None because Google SDK's _get_text() filters thought parts. The handler crashes with ValueError: No content in response: STOP.
  • Tool path (_response_to_result_with_tools): Once Bug 1 is fixed and thought parts are filtered, the content list will be empty, crashing with ValueError: No content in response from completion.

Bug 3: Safety-filtered responses produce unhelpful errors

When Gemini blocks a response (safety filter, recitation, etc.):

  • Non-tool path includes finish_reason: "No content in response: SAFETY" — somewhat helpful
  • Tool path at line 379 does NOT include finish_reason: just "No content in response from completion" — no indication of why

The tool-path error should include candidate.finish_reason so callers can distinguish between "model returned nothing" vs "response was blocked by safety filter".

Reproduction

All three bugs are reproducible without API calls:

from fastmcp.client.sampling.handlers.google_genai import (
    _response_to_result_with_tools,
    _response_to_create_message_result,
)
from google.genai.types import Candidate, Content, GenerateContentResponse, Part

# Bug 1: Thought parts leak as TextContent
response = GenerateContentResponse(candidates=[Candidate(
    content=Content(parts=[Part(text="thinking...", thought=True)], role="model"),
    finish_reason="STOP",
)])
result = _response_to_result_with_tools(response, "test")
assert result.content[0].text == "thinking..."  # BUG: should have been filtered

# Bug 2: Thinking-only response crashes non-tool path
assert response.text is None  # Google SDK filters thoughts in .text
_response_to_create_message_result(response, "test")
# ValueError: No content in response: FinishReason.STOP

# Bug 3: Safety-filtered response — unhelpful error on tool path
response = GenerateContentResponse(candidates=[Candidate(
    content=Content(parts=[], role="model"),
    finish_reason="SAFETY",
)])
_response_to_result_with_tools(response, "test")
# ValueError: "No content in response from completion" — no mention of SAFETY

Version

fastmcp 3.2.3, google-genai 1.72.0

Metadata

Metadata

Assignees

Labels

bugSomething isn't working. Reports of errors, unexpected behavior, or broken functionality.clientRelated to the FastMCP client SDK or client-side functionality.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions