10
10
from mcp .server .fastmcp import Context , FastMCP
11
11
from mcp .server .fastmcp .prompts .base import Message , UserMessage
12
12
from 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
14
14
from mcp .server .session import ServerSession
15
15
from mcp .shared .exceptions import McpError
16
16
from mcp .shared .memory import (
@@ -195,6 +195,10 @@ def image_tool_fn(path: str) -> Image:
195
195
return Image (path )
196
196
197
197
198
+ def audio_tool_fn (path : str ) -> Audio :
199
+ return Audio (path )
200
+
201
+
198
202
def mixed_content_tool_fn () -> list [ContentBlock ]:
199
203
return [
200
204
TextContent (type = "text" , text = "Hello" ),
@@ -300,6 +304,60 @@ async def test_tool_image_helper(self, tmp_path: Path):
300
304
# Check structured content - Image return type should NOT have structured output
301
305
assert result .structuredContent is None
302
306
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
+
303
361
@pytest .mark .anyio
304
362
async def test_tool_mixed_content (self ):
305
363
mcp = FastMCP ()
@@ -332,19 +390,24 @@ async def test_tool_mixed_content(self):
332
390
assert structured_result [i ][key ] == value
333
391
334
392
@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 ):
336
394
"""Test that lists containing Image objects and other types are handled
337
395
correctly"""
338
396
# Create a test image
339
397
image_path = tmp_path / "test.png"
340
398
image_path .write_bytes (b"test image data" )
341
399
400
+ # Create a test audio
401
+ audio_path = tmp_path / "test.wav"
402
+ audio_path .write_bytes (b"test audio data" )
403
+
342
404
# TODO(Marcelo): It seems if we add the proper type hint, it generates an invalid JSON schema.
343
405
# We need to fix this.
344
406
def mixed_list_fn () -> list : # type: ignore
345
407
return [ # type: ignore
346
408
"text message" ,
347
409
Image (image_path ),
410
+ Audio (audio_path ),
348
411
{"key" : "value" },
349
412
TextContent (type = "text" , text = "direct content" ),
350
413
]
@@ -353,7 +416,7 @@ def mixed_list_fn() -> list: # type: ignore
353
416
mcp .add_tool (mixed_list_fn ) # type: ignore
354
417
async with client_session (mcp ._mcp_server ) as client :
355
418
result = await client .call_tool ("mixed_list_fn" , {})
356
- assert len (result .content ) == 4
419
+ assert len (result .content ) == 5
357
420
# Check text conversion
358
421
content1 = result .content [0 ]
359
422
assert isinstance (content1 , TextContent )
@@ -363,14 +426,19 @@ def mixed_list_fn() -> list: # type: ignore
363
426
assert isinstance (content2 , ImageContent )
364
427
assert content2 .mimeType == "image/png"
365
428
assert base64 .b64decode (content2 .data ) == b"test image data"
366
- # Check dict conversion
429
+ # Check audio conversion
367
430
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
371
435
content4 = result .content [3 ]
372
436
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"
374
442
# Check structured content - untyped list with Image objects should NOT have structured output
375
443
assert result .structuredContent is None
376
444
0 commit comments