Skip to content

Commit d129a7f

Browse files
committed
fix(mcp): handle unsupported image and blob mime types in tool results
1 parent b340dc4 commit d129a7f

2 files changed

Lines changed: 83 additions & 7 deletions

File tree

src/strands/tools/mcp/mcp_client.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,11 @@ def _map_mcp_content_to_tool_result_content(
874874
return {"text": content.text}
875875
elif isinstance(content, MCPImageContent):
876876
self._log_debug_with_thread("mapping MCP image content with mime type: %s", content.mimeType)
877+
if content.mimeType not in MIME_TO_FORMAT:
878+
logger.warning(
879+
"mime_type=<%s> | unsupported mcp image mime type, falling back to json", content.mimeType
880+
)
881+
return {"json": content.model_dump(exclude_none=True)}
877882
return {
878883
"image": {
879884
"format": MIME_TO_FORMAT[content.mimeType],
@@ -900,8 +905,8 @@ def _map_mcp_content_to_tool_result_content(
900905
try:
901906
raw_bytes = base64.b64decode(resource.blob)
902907
except Exception:
903-
self._log_debug_with_thread("embedded resource blob could not be decoded - dropping")
904-
return None
908+
logger.warning("embedded resource blob could not be decoded, falling back to json")
909+
return {"json": content.model_dump(exclude_none=True)}
905910

906911
if resource.mimeType and (
907912
resource.mimeType.startswith("text/")
@@ -928,8 +933,11 @@ def _map_mcp_content_to_tool_result_content(
928933
}
929934
}
930935

931-
self._log_debug_with_thread("embedded resource blob with non-textual/unknown mimeType - dropping")
932-
return None
936+
logger.warning(
937+
"mime_type=<%s> | unsupported mcp resource blob mime type, falling back to json",
938+
resource.mimeType,
939+
)
940+
return {"json": content.model_dump(exclude_none=True)}
933941

934942
return None # type: ignore[unreachable] # Defensive: future MCP resource types
935943
else:

tests/strands/tools/mcp/test_mcp_client.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -728,8 +728,8 @@ def test_call_tool_sync_embedded_image_blob(mock_transport, mock_session):
728728
assert "bytes" in result["content"][0]["image"]["source"]
729729

730730

731-
def test_call_tool_sync_embedded_non_textual_blob_dropped(mock_transport, mock_session):
732-
"""EmbeddedResource.resource (blob with non-textual/unknown MIME) should be dropped."""
731+
def test_call_tool_sync_embedded_non_textual_blob_falls_back_to_json(mock_transport, mock_session):
732+
"""EmbeddedResource.resource (blob with non-textual/unknown MIME) should fall back to json."""
733733
payload = base64.b64encode(b"\x00\x01\x02\x03").decode()
734734

735735
embedded_resource = {
@@ -747,7 +747,75 @@ def test_call_tool_sync_embedded_non_textual_blob_dropped(mock_transport, mock_s
747747

748748
mock_session.call_tool.assert_called_once_with("get_file_contents", {}, None, meta=None)
749749
assert result["status"] == "success"
750-
assert len(result["content"]) == 0 # Content should be dropped
750+
assert len(result["content"]) == 1
751+
assert "json" in result["content"][0]
752+
assert result["content"][0]["json"]["resource"]["mimeType"] == "application/octet-stream"
753+
assert result["content"][0]["json"]["resource"]["blob"] == payload
754+
755+
756+
def test_call_tool_sync_image_content_supported_mime(mock_transport, mock_session):
757+
"""MCPImageContent with a supported MIME type should map to image content."""
758+
payload = base64.b64encode(b"\x89PNG\r\n\x1a\n").decode()
759+
760+
image_content = {
761+
"type": "image",
762+
"data": payload,
763+
"mimeType": "image/png",
764+
}
765+
mock_session.call_tool.return_value = MCPCallToolResult(isError=False, content=[image_content])
766+
767+
with MCPClient(mock_transport["transport_callable"]) as client:
768+
result = client.call_tool_sync(tool_use_id="img-png", name="get_image", arguments={})
769+
770+
mock_session.call_tool.assert_called_once_with("get_image", {}, None, meta=None)
771+
assert result["status"] == "success"
772+
assert len(result["content"]) == 1
773+
assert "image" in result["content"][0]
774+
assert result["content"][0]["image"]["format"] == "png"
775+
assert "bytes" in result["content"][0]["image"]["source"]
776+
777+
778+
def test_call_tool_sync_image_content_unsupported_mime_falls_back_to_json(mock_transport, mock_session):
779+
"""MCPImageContent with an unsupported MIME (e.g. image/bmp) should fall back to json instead of crashing."""
780+
payload = base64.b64encode(b"\x00\x01\x02\x03").decode()
781+
782+
image_content = {
783+
"type": "image",
784+
"data": payload,
785+
"mimeType": "image/bmp",
786+
}
787+
mock_session.call_tool.return_value = MCPCallToolResult(isError=False, content=[image_content])
788+
789+
with MCPClient(mock_transport["transport_callable"]) as client:
790+
result = client.call_tool_sync(tool_use_id="img-bmp", name="get_image", arguments={})
791+
792+
mock_session.call_tool.assert_called_once_with("get_image", {}, None, meta=None)
793+
assert result["status"] == "success"
794+
assert len(result["content"]) == 1
795+
assert "json" in result["content"][0]
796+
assert result["content"][0]["json"]["mimeType"] == "image/bmp"
797+
assert result["content"][0]["json"]["data"] == payload
798+
799+
800+
def test_call_tool_sync_embedded_blob_decode_failure_falls_back_to_json(mock_transport, mock_session):
801+
"""EmbeddedResource.resource with an undecodable blob should fall back to json."""
802+
embedded_resource = {
803+
"type": "resource",
804+
"resource": {
805+
"uri": "mcp://resource/bad-blob",
806+
"blob": "!!!not-valid-base64!!!",
807+
"mimeType": "image/png",
808+
},
809+
}
810+
mock_session.call_tool.return_value = MCPCallToolResult(isError=False, content=[embedded_resource])
811+
812+
with MCPClient(mock_transport["transport_callable"]) as client:
813+
result = client.call_tool_sync(tool_use_id="er-bad", name="get_file_contents", arguments={})
814+
815+
mock_session.call_tool.assert_called_once_with("get_file_contents", {}, None, meta=None)
816+
assert result["status"] == "success"
817+
assert len(result["content"]) == 1
818+
assert "json" in result["content"][0]
751819

752820

753821
def test_call_tool_sync_embedded_multiple_textual_mimes(mock_transport, mock_session):

0 commit comments

Comments
 (0)