|
| 1 | +import base64 |
1 | 2 | import time
|
2 | 3 | from unittest.mock import AsyncMock, MagicMock, patch
|
3 | 4 |
|
@@ -541,3 +542,149 @@ def slow_transport():
|
541 | 542 | assert client._background_thread_session is None
|
542 | 543 | assert client._background_thread_event_loop is None
|
543 | 544 | assert not client._init_future.done() # New future created
|
| 545 | + |
| 546 | + |
| 547 | +def test_call_tool_sync_embedded_nested_text(mock_transport, mock_session): |
| 548 | + """EmbeddedResource.resource (uri + text) should map to plain text content.""" |
| 549 | + embedded_resource = { |
| 550 | + "type": "resource", # required literal |
| 551 | + "resource": { |
| 552 | + "uri": "mcp://resource/embedded-text-1", |
| 553 | + "text": "inner text", |
| 554 | + "mimeType": "text/plain", |
| 555 | + }, |
| 556 | + } |
| 557 | + mock_session.call_tool.return_value = MCPCallToolResult(isError=False, content=[embedded_resource]) |
| 558 | + |
| 559 | + with MCPClient(mock_transport["transport_callable"]) as client: |
| 560 | + result = client.call_tool_sync(tool_use_id="er-text", name="get_file_contents", arguments={}) |
| 561 | + |
| 562 | + mock_session.call_tool.assert_called_once_with("get_file_contents", {}, None) |
| 563 | + assert result["status"] == "success" |
| 564 | + assert len(result["content"]) == 1 |
| 565 | + assert result["content"][0]["text"] == "inner text" |
| 566 | + |
| 567 | + |
| 568 | +def test_call_tool_sync_embedded_nested_base64_textual_mime(mock_transport, mock_session): |
| 569 | + """EmbeddedResource.resource (uri + blob with textual MIME) should decode to text.""" |
| 570 | + |
| 571 | + payload = base64.b64encode(b'{"k":"v"}').decode() |
| 572 | + |
| 573 | + embedded_resource = { |
| 574 | + "type": "resource", |
| 575 | + "resource": { |
| 576 | + "uri": "mcp://resource/embedded-blob-1", |
| 577 | + # NOTE: blob is a STRING, mimeType is sibling |
| 578 | + "blob": payload, |
| 579 | + "mimeType": "application/json", |
| 580 | + }, |
| 581 | + } |
| 582 | + mock_session.call_tool.return_value = MCPCallToolResult(isError=False, content=[embedded_resource]) |
| 583 | + |
| 584 | + with MCPClient(mock_transport["transport_callable"]) as client: |
| 585 | + result = client.call_tool_sync(tool_use_id="er-blob", name="get_file_contents", arguments={}) |
| 586 | + |
| 587 | + mock_session.call_tool.assert_called_once_with("get_file_contents", {}, None) |
| 588 | + assert result["status"] == "success" |
| 589 | + assert len(result["content"]) == 1 |
| 590 | + assert result["content"][0]["text"] == '{"k":"v"}' |
| 591 | + |
| 592 | + |
| 593 | +def test_call_tool_sync_embedded_image_blob(mock_transport, mock_session): |
| 594 | + """EmbeddedResource.resource (blob with image MIME) should map to image content.""" |
| 595 | + # Read yellow.png file |
| 596 | + with open("tests_integ/yellow.png", "rb") as image_file: |
| 597 | + png_data = image_file.read() |
| 598 | + payload = base64.b64encode(png_data).decode() |
| 599 | + |
| 600 | + embedded_resource = { |
| 601 | + "type": "resource", |
| 602 | + "resource": { |
| 603 | + "uri": "mcp://resource/embedded-image", |
| 604 | + "blob": payload, |
| 605 | + "mimeType": "image/png", |
| 606 | + }, |
| 607 | + } |
| 608 | + mock_session.call_tool.return_value = MCPCallToolResult(isError=False, content=[embedded_resource]) |
| 609 | + |
| 610 | + with MCPClient(mock_transport["transport_callable"]) as client: |
| 611 | + result = client.call_tool_sync(tool_use_id="er-image", name="get_file_contents", arguments={}) |
| 612 | + |
| 613 | + mock_session.call_tool.assert_called_once_with("get_file_contents", {}, None) |
| 614 | + assert result["status"] == "success" |
| 615 | + assert len(result["content"]) == 1 |
| 616 | + assert "image" in result["content"][0] |
| 617 | + assert result["content"][0]["image"]["format"] == "png" |
| 618 | + assert "bytes" in result["content"][0]["image"]["source"] |
| 619 | + |
| 620 | + |
| 621 | +def test_call_tool_sync_embedded_non_textual_blob_dropped(mock_transport, mock_session): |
| 622 | + """EmbeddedResource.resource (blob with non-textual/unknown MIME) should be dropped.""" |
| 623 | + payload = base64.b64encode(b"\x00\x01\x02\x03").decode() |
| 624 | + |
| 625 | + embedded_resource = { |
| 626 | + "type": "resource", |
| 627 | + "resource": { |
| 628 | + "uri": "mcp://resource/embedded-binary", |
| 629 | + "blob": payload, |
| 630 | + "mimeType": "application/octet-stream", |
| 631 | + }, |
| 632 | + } |
| 633 | + mock_session.call_tool.return_value = MCPCallToolResult(isError=False, content=[embedded_resource]) |
| 634 | + |
| 635 | + with MCPClient(mock_transport["transport_callable"]) as client: |
| 636 | + result = client.call_tool_sync(tool_use_id="er-binary", name="get_file_contents", arguments={}) |
| 637 | + |
| 638 | + mock_session.call_tool.assert_called_once_with("get_file_contents", {}, None) |
| 639 | + assert result["status"] == "success" |
| 640 | + assert len(result["content"]) == 0 # Content should be dropped |
| 641 | + |
| 642 | + |
| 643 | +def test_call_tool_sync_embedded_multiple_textual_mimes(mock_transport, mock_session): |
| 644 | + """EmbeddedResource with different textual MIME types should decode to text.""" |
| 645 | + |
| 646 | + # Test YAML content |
| 647 | + yaml_content = base64.b64encode(b"key: value\nlist:\n - item1\n - item2").decode() |
| 648 | + embedded_resource = { |
| 649 | + "type": "resource", |
| 650 | + "resource": { |
| 651 | + "uri": "mcp://resource/embedded-yaml", |
| 652 | + "blob": yaml_content, |
| 653 | + "mimeType": "application/yaml", |
| 654 | + }, |
| 655 | + } |
| 656 | + mock_session.call_tool.return_value = MCPCallToolResult(isError=False, content=[embedded_resource]) |
| 657 | + |
| 658 | + with MCPClient(mock_transport["transport_callable"]) as client: |
| 659 | + result = client.call_tool_sync(tool_use_id="er-yaml", name="get_file_contents", arguments={}) |
| 660 | + |
| 661 | + mock_session.call_tool.assert_called_once_with("get_file_contents", {}, None) |
| 662 | + assert result["status"] == "success" |
| 663 | + assert len(result["content"]) == 1 |
| 664 | + assert "key: value" in result["content"][0]["text"] |
| 665 | + |
| 666 | + |
| 667 | +def test_call_tool_sync_embedded_unknown_resource_type_dropped(mock_transport, mock_session): |
| 668 | + """EmbeddedResource with unknown resource type should be dropped for forward compatibility.""" |
| 669 | + |
| 670 | + # Mock an unknown resource type that's neither TextResourceContents nor BlobResourceContents |
| 671 | + class UnknownResourceContents: |
| 672 | + def __init__(self): |
| 673 | + self.uri = "mcp://resource/unknown-type" |
| 674 | + self.mimeType = "application/unknown" |
| 675 | + self.data = "some unknown data" |
| 676 | + |
| 677 | + # Create a mock embedded resource with unknown resource type |
| 678 | + mock_embedded_resource = MagicMock() |
| 679 | + mock_embedded_resource.resource = UnknownResourceContents() |
| 680 | + |
| 681 | + mock_session.call_tool.return_value = MagicMock( |
| 682 | + isError=False, content=[mock_embedded_resource], structuredContent=None |
| 683 | + ) |
| 684 | + |
| 685 | + with MCPClient(mock_transport["transport_callable"]) as client: |
| 686 | + result = client.call_tool_sync(tool_use_id="er-unknown", name="get_file_contents", arguments={}) |
| 687 | + |
| 688 | + mock_session.call_tool.assert_called_once_with("get_file_contents", {}, None) |
| 689 | + assert result["status"] == "success" |
| 690 | + assert len(result["content"]) == 0 # Unknown resource type should be dropped |
0 commit comments