11from __future__ import annotations as _annotations
22
33import asyncio
4+ import base64
45import re
56import secrets
67import subprocess
2021from mcp .client .sse import sse_client
2122from mcp .client .stdio import stdio_client
2223from mcp .client .streamable_http import streamablehttp_client
24+ from pydantic import FileUrl
2325
2426if TYPE_CHECKING :
2527 from mcp import ClientSession
@@ -46,6 +48,8 @@ class McpTools(StrEnum):
4648On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains.
4749"""
4850
51+ BASE_64_IMAGE = 'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAAEX0lEQVR4nOzdO8vX9R/HcS/56f8PWotGQkPBBUWESCQYNJR0GjIn6UBTgUMZTiGE4ZgRVKNkuDSEFtgBQqIiKunkEFdkWLmEBQUWiNUQYd2KNwTPx+MGvD7Tk/f2/S7O7tmyatKnJx8b3f/p6EOj+5euu2Z0/+Sxt0f3N++9fHR/+57/j+7vuPuT0f3Vo+vwHycA0gRAmgBIEwBpAiBNAKQJgDQBkCYA0gRAmgBIEwBpAiBNAKQJgDQBkCYA0gRAmgBIEwBpAiBNAKQJgDQBkCYA0gRAmgBIEwBpAiBNAKQtDr561+gDpzf9PLp/4eNzo/uXzv41uv/BM0+O7h9/bsPo/vqPdo3u7965GN13AUgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSlh5ce+XoA9+eODK6v3r7naP7b31zaHT/4p+3jO4f2/Tb6P7K41tH9zff+8LovgtAmgBIEwBpAiBNAKQJgDQBkCYA0gRAmgBIEwBpAiBNAKQJgDQBkCYA0gRAmgBIEwBpAiBNAKQJgDQBkCYA0gRAmgBIEwBpAiBNAKQJgDQBkLb09ZmLow8sb1ke3d92YXR+1dO7PhzdX7f2xtH9Q5fN/t/g2j9eHt3/cc350X0XgDQBkCYA0gRAmgBIEwBpAiBNAKQJgDQBkCYA0gRAmgBIEwBpAiBNAKQJgDQBkCYA0gRAmgBIEwBpAiBNAKQJgDQBkCYA0gRAmgBIEwBpAiBtcf3eW0cfePTE7Pf1D9yxMrq/4YrR+VWvnN84uv/lvs2j+2v3nx3dv3rT/0b3XQDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmAtKWrzq0ffeD312f339h5ZnT/npsPj+7//cPDo/un739idP/Xg5+P7j/y/G2j+y4AaQIgTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIEQNpi/5FfRh94753XRvcP7F0zuv/V7e+O7t906v3R/WdP/zO6f9/ixdH9G3Z/NrrvApAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkCYA0AZAmANIEQJoASBMAaQIgTQCkLb25vDL6wLoHjo7ur7z03ej++u+fGt0/vm/2+/dfHF4e3d9xauPo/taN20b3XQDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmANAGQJgDSBECaAEgTAGkCIE0ApAmAtH8DAAD//9drYGg9ROu9AAAAAElFTkSuQmCC'
52+
4953
5054@pytest .fixture
5155def anyio_backend ():
@@ -372,6 +376,61 @@ async def test_upload_files(
372376 assert createdFile .is_file ()
373377 assert createdFile .read_text () == LOREM_IPSUM
374378
379+ @pytest .mark .parametrize ('content_type' , ['bytes' , 'text' ])
380+ async def test_download_files (
381+ self ,
382+ mcp_session : ClientSession ,
383+ server_type : Literal ['stdio' , 'sse' , 'streamable_http' ],
384+ mount : bool | str ,
385+ content_type : Literal ['bytes' , 'text' ],
386+ ):
387+ if mount is False :
388+ pytest .skip ('No directory mounted.' )
389+ result = await mcp_session .initialize ()
390+
391+ # Extract directory from response
392+ storageDir = self .get_dir_from_instructions (result .instructions )
393+ assert storageDir .is_dir ()
394+
395+ match content_type :
396+ case 'bytes' :
397+ filename = 'image.png'
398+ ctype = 'image/png'
399+ file_path = storageDir / filename
400+ file_path .write_bytes (base64 .b64decode (BASE_64_IMAGE ))
401+
402+ case 'text' :
403+ filename = 'lorem.txt'
404+ ctype = 'text/plain'
405+ file_path = storageDir / filename
406+ file_path .write_text (LOREM_IPSUM )
407+
408+ result = await mcp_session .list_resources ()
409+
410+ assert len (result .resources ) == 1
411+ resource = result .resources [0 ]
412+ assert resource .name == filename
413+ assert resource .mimeType is not None
414+ assert resource .mimeType .startswith (ctype )
415+ assert str (resource .uri ) == f'file:///{ filename } '
416+
417+ result = await mcp_session .read_resource (FileUrl (f'file:///{ filename } ' ))
418+
419+ assert len (result .contents ) == 1
420+ resource = result .contents [0 ]
421+ assert str (resource .uri ) == f'file:///{ filename } '
422+ assert resource .mimeType is not None
423+ assert resource .mimeType .startswith (ctype )
424+
425+ match content_type :
426+ case 'bytes' :
427+ assert isinstance (resource , types .BlobResourceContents )
428+ assert resource .blob == BASE_64_IMAGE
429+
430+ case 'text' :
431+ assert isinstance (resource , types .TextResourceContents )
432+ assert resource .text == LOREM_IPSUM
433+
375434
376435async def test_install_run_python_code () -> None :
377436 node_modules = Path (__file__ ).parent / 'node_modules'
0 commit comments