diff --git a/backend/packages/harness/deerflow/mcp/tools.py b/backend/packages/harness/deerflow/mcp/tools.py index 19f5ffcec5..b77ef85129 100644 --- a/backend/packages/harness/deerflow/mcp/tools.py +++ b/backend/packages/harness/deerflow/mcp/tools.py @@ -604,7 +604,17 @@ def _convert_call_tool_result( if mime and mime.startswith("image/"): lc_content.append(create_image_block(base64=res.blob, mime_type=mime)) else: - lc_content.append(create_file_block(base64=res.blob, mime_type=mime)) + # ``BlobResourceContents.mimeType`` is optional in the MCP spec, but + # langchain-core refuses a base64 block without one. Degrade to a + # generic binary type instead of raising, so the rest of the result — + # including the text blocks the server returned — survives. The + # sibling ``ResourceLink`` branch already degrades the same way. + lc_content.append( + create_file_block( + base64=res.blob, + mime_type=mime or "application/octet-stream", + ) + ) else: lc_content.append(create_text_block(text=str(res))) else: diff --git a/backend/tests/test_mcp_blob_missing_mime_type.py b/backend/tests/test_mcp_blob_missing_mime_type.py new file mode 100644 index 0000000000..b1c0aa5d88 --- /dev/null +++ b/backend/tests/test_mcp_blob_missing_mime_type.py @@ -0,0 +1,51 @@ +"""An MCP blob without a mimeType must degrade, not fail the whole tool call.""" + +import base64 + +from mcp.types import BlobResourceContents, CallToolResult, EmbeddedResource, TextContent + +from deerflow.mcp.tools import _convert_call_tool_result + +BLOB = base64.b64encode(b"binary payload").decode("ascii") + + +def _result(mime_type: str | None, *, with_text: bool = True) -> CallToolResult: + content = [TextContent(type="text", text="here is your report")] if with_text else [] + content.append( + EmbeddedResource( + type="resource", + resource=BlobResourceContents(uri="file:///tmp/report.bin", blob=BLOB, mimeType=mime_type), + ) + ) + return CallToolResult(content=content, isError=False) + + +def test_blob_without_mime_type_degrades_to_octet_stream(): + lc_content, _artifact = _convert_call_tool_result(_result(None)) + + assert [block["type"] for block in lc_content] == ["text", "file"] + assert lc_content[0]["text"] == "here is your report" + assert lc_content[1]["mime_type"] == "application/octet-stream" + assert lc_content[1]["base64"] == BLOB + + +def test_text_blocks_survive_the_fallback(): + lc_content, _artifact = _convert_call_tool_result(_result(None)) + + assert [block["type"] for block in lc_content] == ["text", "file"] + assert lc_content[0]["text"] == "here is your report" + assert lc_content[1]["mime_type"] == "application/octet-stream" + + +def test_declared_non_image_mime_type_is_preserved(): + lc_content, _artifact = _convert_call_tool_result(_result("application/pdf")) + + assert lc_content[1]["type"] == "file" + assert lc_content[1]["mime_type"] == "application/pdf" + + +def test_declared_image_mime_type_still_becomes_an_image_block(): + lc_content, _artifact = _convert_call_tool_result(_result("image/png")) + + assert [block["type"] for block in lc_content] == ["text", "image"] + assert lc_content[1]["mime_type"] == "image/png"