mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 13:34:17 +02:00
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:
parent
f26c8fad3e
commit
d6b55c0b2f
4 changed files with 61 additions and 0 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue