From d6b55c0b2fe252c30f69e4f9430dfcf9eb9be784 Mon Sep 17 00:00:00 2001 From: Bill Easton Date: Sun, 12 Apr 2026 11:52:31 -0500 Subject: [PATCH] Raise on unhandled content types in sampling handler dispatch chains (#3857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * 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) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .../client/sampling/handlers/anthropic.py | 4 +++ .../client/sampling/handlers/openai.py | 4 +++ .../handlers/test_anthropic_handler.py | 28 +++++++++++++++++++ .../sampling/handlers/test_openai_handler.py | 25 +++++++++++++++++ 4 files changed, 61 insertions(+) diff --git a/src/fastmcp/client/sampling/handlers/anthropic.py b/src/fastmcp/client/sampling/handlers/anthropic.py index b7a6ce090..0939121d1 100644 --- a/src/fastmcp/client/sampling/handlers/anthropic.py +++ b/src/fastmcp/client/sampling/handlers/anthropic.py @@ -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( diff --git a/src/fastmcp/client/sampling/handlers/openai.py b/src/fastmcp/client/sampling/handlers/openai.py index ffc40f158..bbd6a0ae5 100644 --- a/src/fastmcp/client/sampling/handlers/openai.py +++ b/src/fastmcp/client/sampling/handlers/openai.py @@ -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 diff --git a/tests/client/sampling/handlers/test_anthropic_handler.py b/tests/client/sampling/handlers/test_anthropic_handler.py index 5910eb92b..757ec2eeb 100644 --- a/tests/client/sampling/handlers/test_anthropic_handler.py +++ b/tests/client/sampling/handlers/test_anthropic_handler.py @@ -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]) diff --git a/tests/client/sampling/handlers/test_openai_handler.py b/tests/client/sampling/handlers/test_openai_handler.py index e80ba3292..4cfb9d606 100644 --- a/tests/client/sampling/handlers/test_openai_handler.py +++ b/tests/client/sampling/handlers/test_openai_handler.py @@ -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])