1010from mcp .server .fastmcp import Context , FastMCP
1111from mcp .server .fastmcp .prompts .base import Message , UserMessage
1212from mcp .server .fastmcp .resources import FileResource , FunctionResource
13- from mcp .server .fastmcp .utilities .types import Image
13+ from mcp .server .fastmcp .utilities .types import Audio , Image
1414from mcp .server .session import ServerSession
1515from mcp .shared .exceptions import McpError
1616from mcp .shared .memory import (
@@ -195,6 +195,10 @@ def image_tool_fn(path: str) -> Image:
195195 return Image (path )
196196
197197
198+ def audio_tool_fn (path : str ) -> Audio :
199+ return Audio (path )
200+
201+
198202def mixed_content_tool_fn () -> list [ContentBlock ]:
199203 return [
200204 TextContent (type = "text" , text = "Hello" ),
@@ -300,6 +304,60 @@ async def test_tool_image_helper(self, tmp_path: Path):
300304 # Check structured content - Image return type should NOT have structured output
301305 assert result .structuredContent is None
302306
307+ @pytest .mark .anyio
308+ async def test_tool_audio_helper (self , tmp_path : Path ):
309+ # Create a test audio
310+ audio_path = tmp_path / "test.wav"
311+ audio_path .write_bytes (b"fake wav data" )
312+
313+ mcp = FastMCP ()
314+ mcp .add_tool (audio_tool_fn )
315+ async with client_session (mcp ._mcp_server ) as client :
316+ result = await client .call_tool ("audio_tool_fn" , {"path" : str (audio_path )})
317+ assert len (result .content ) == 1
318+ content = result .content [0 ]
319+ assert isinstance (content , AudioContent )
320+ assert content .type == "audio"
321+ assert content .mimeType == "audio/wav"
322+ # Verify base64 encoding
323+ decoded = base64 .b64decode (content .data )
324+ assert decoded == b"fake wav data"
325+ # Check structured content - Image return type should NOT have structured output
326+ assert result .structuredContent is None
327+
328+ @pytest .mark .parametrize (
329+ "filename,expected_mime_type" ,
330+ [
331+ ("test.wav" , "audio/wav" ),
332+ ("test.mp3" , "audio/mpeg" ),
333+ ("test.ogg" , "audio/ogg" ),
334+ ("test.flac" , "audio/flac" ),
335+ ("test.aac" , "audio/aac" ),
336+ ("test.m4a" , "audio/mp4" ),
337+ ("test.unknown" , "application/octet-stream" ), # Unknown extension fallback
338+ ],
339+ )
340+ @pytest .mark .anyio
341+ async def test_tool_audio_suffix_detection (self , tmp_path : Path , filename : str , expected_mime_type : str ):
342+ """Test that Audio helper correctly detects MIME types from file suffixes"""
343+ mcp = FastMCP ()
344+ mcp .add_tool (audio_tool_fn )
345+
346+ # Create a test audio file with the specific extension
347+ audio_path = tmp_path / filename
348+ audio_path .write_bytes (b"fake audio data" )
349+
350+ async with client_session (mcp ._mcp_server ) as client :
351+ result = await client .call_tool ("audio_tool_fn" , {"path" : str (audio_path )})
352+ assert len (result .content ) == 1
353+ content = result .content [0 ]
354+ assert isinstance (content , AudioContent )
355+ assert content .type == "audio"
356+ assert content .mimeType == expected_mime_type
357+ # Verify base64 encoding
358+ decoded = base64 .b64decode (content .data )
359+ assert decoded == b"fake audio data"
360+
303361 @pytest .mark .anyio
304362 async def test_tool_mixed_content (self ):
305363 mcp = FastMCP ()
@@ -332,19 +390,24 @@ async def test_tool_mixed_content(self):
332390 assert structured_result [i ][key ] == value
333391
334392 @pytest .mark .anyio
335- async def test_tool_mixed_list_with_image (self , tmp_path : Path ):
393+ async def test_tool_mixed_list_with_audio_and_image (self , tmp_path : Path ):
336394 """Test that lists containing Image objects and other types are handled
337395 correctly"""
338396 # Create a test image
339397 image_path = tmp_path / "test.png"
340398 image_path .write_bytes (b"test image data" )
341399
400+ # Create a test audio
401+ audio_path = tmp_path / "test.wav"
402+ audio_path .write_bytes (b"test audio data" )
403+
342404 # TODO(Marcelo): It seems if we add the proper type hint, it generates an invalid JSON schema.
343405 # We need to fix this.
344406 def mixed_list_fn () -> list : # type: ignore
345407 return [ # type: ignore
346408 "text message" ,
347409 Image (image_path ),
410+ Audio (audio_path ),
348411 {"key" : "value" },
349412 TextContent (type = "text" , text = "direct content" ),
350413 ]
@@ -353,7 +416,7 @@ def mixed_list_fn() -> list: # type: ignore
353416 mcp .add_tool (mixed_list_fn ) # type: ignore
354417 async with client_session (mcp ._mcp_server ) as client :
355418 result = await client .call_tool ("mixed_list_fn" , {})
356- assert len (result .content ) == 4
419+ assert len (result .content ) == 5
357420 # Check text conversion
358421 content1 = result .content [0 ]
359422 assert isinstance (content1 , TextContent )
@@ -363,14 +426,19 @@ def mixed_list_fn() -> list: # type: ignore
363426 assert isinstance (content2 , ImageContent )
364427 assert content2 .mimeType == "image/png"
365428 assert base64 .b64decode (content2 .data ) == b"test image data"
366- # Check dict conversion
429+ # Check audio conversion
367430 content3 = result .content [2 ]
368- assert isinstance (content3 , TextContent )
369- assert '"key": "value"' in content3 .text
370- # Check direct TextContent
431+ assert isinstance (content3 , AudioContent )
432+ assert content3 .mimeType == "audio/wav"
433+ assert base64 .b64decode (content3 .data ) == b"test audio data"
434+ # Check dict conversion
371435 content4 = result .content [3 ]
372436 assert isinstance (content4 , TextContent )
373- assert content4 .text == "direct content"
437+ assert '"key": "value"' in content4 .text
438+ # Check direct TextContent
439+ content5 = result .content [4 ]
440+ assert isinstance (content5 , TextContent )
441+ assert content5 .text == "direct content"
374442 # Check structured content - untyped list with Image objects should NOT have structured output
375443 assert result .structuredContent is None
376444
0 commit comments