Raise on unhandled content types in sampling handler dispatch chains (#3857)

* Raise on unhandled content types in sampling handler dispatch chains

The Anthropic and OpenAI sampling handlers have isinstance chains that
dispatch on MCP content types but silently drop unhandled variants like
EmbeddedResource and ResourceLink. This adds explicit else-raise guards
to match the Gemini handler's behavior and the single-content dispatch
paths that already raise.

Raising is the right choice over warn-and-skip: a partial conversion
produces a plausible-but-wrong LLM response (the model confidently
answers based on incomplete input), which is worse than a clear error
that tells the user exactly what isn't supported.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add tests for unsupported content type raises in sampling handlers

Tests the new ValueError raises for unsupported content types
(e.g. EmbeddedResource) in the Anthropic and OpenAI message
conversion loops. Uses model_construct to bypass Pydantic's
union validation since the raise is a defensive guard for
future SDK content types.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bill Easton 2026-04-12 11:52:31 -05:00 committed by GitHub
commit d6b55c0b2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 61 additions and 0 deletions

View file

@ -235,6 +235,10 @@ class AnthropicSamplingHandler:
is_error=item.isError if item.isError else False,
)
)
else:
raise ValueError(
f"Unsupported content type for Anthropic: {type(item).__name__}"
)
if content_blocks:
anthropic_messages.append(

View file

@ -243,6 +243,10 @@ class OpenAISamplingHandler:
content=content_text,
)
)
else:
raise ValueError(
f"Unsupported content type for OpenAI: {type(item).__name__}"
)
# Add assistant message with tool calls if present
# OpenAI requires: assistant (with tool_calls) -> tool messages

View file

@ -8,14 +8,17 @@ from mcp.types import (
AudioContent,
CreateMessageResult,
CreateMessageResultWithTools,
EmbeddedResource,
ImageContent,
ModelHint,
ModelPreferences,
SamplingMessage,
TextContent,
TextResourceContents,
ToolResultContent,
ToolUseContent,
)
from pydantic import AnyUrl
from fastmcp.client.sampling.handlers.anthropic import (
AnthropicSamplingHandler,
@ -372,3 +375,28 @@ def test_convert_messages_with_tool_result_content():
"is_error": False,
}
]
def test_convert_messages_raises_on_unsupported_content_type():
"""Unsupported content types should raise ValueError.
SamplingMessage validates content against a union of known types, so
we use model_construct to bypass validation and simulate a future
SDK content type that the handler doesn't know about yet.
"""
embedded = EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri=AnyUrl("file:///test.txt"), text="hello", mimeType="text/plain"
),
)
# Must be inside a list content — single-content messages hit a
# different check. Use model_construct to bypass Pydantic's
# union validation (EmbeddedResource is not in the content union).
msg = SamplingMessage.model_construct(
role="user",
content=[TextContent(type="text", text="prefix"), embedded],
)
with pytest.raises(ValueError, match="Unsupported content type for Anthropic"):
AnthropicSamplingHandler._convert_to_anthropic_messages([msg])

View file

@ -6,11 +6,13 @@ from mcp.types import (
AudioContent,
CreateMessageRequestParams,
CreateMessageResult,
EmbeddedResource,
ImageContent,
ModelHint,
ModelPreferences,
SamplingMessage,
TextContent,
TextResourceContents,
ToolUseContent,
)
from openai import AsyncOpenAI
@ -25,6 +27,7 @@ from openai.types.chat import (
ChatCompletionUserMessageParam,
)
from openai.types.chat.chat_completion import Choice
from pydantic import AnyUrl
from fastmcp.client.sampling.handlers.openai import (
OpenAISamplingHandler,
@ -316,3 +319,25 @@ async def test_chat_completion_to_create_message_result():
role="assistant",
model="gpt-4o-mini",
)
def test_convert_messages_raises_on_unsupported_content_type():
"""Unsupported content types should raise ValueError.
SamplingMessage validates content against a union of known types, so
we use model_construct to bypass validation and simulate a future
SDK content type that the handler doesn't know about yet.
"""
embedded = EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri=AnyUrl("file:///test.txt"), text="hello", mimeType="text/plain"
),
)
msg = SamplingMessage.model_construct(
role="user",
content=[TextContent(type="text", text="prefix"), embedded],
)
with pytest.raises(ValueError, match="Unsupported content type for OpenAI"):
OpenAISamplingHandler._convert_to_openai_messages(None, [msg])