-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Added Audio to FastMCP #1130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Added Audio to FastMCP #1130
Changes from all commits
992de10
d35a23b
b3ee109
40294d5
b905460
b8f3561
494f58a
1a043a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ | |
from mcp.server.fastmcp import Context, FastMCP | ||
from mcp.server.fastmcp.prompts.base import Message, UserMessage | ||
from mcp.server.fastmcp.resources import FileResource, FunctionResource | ||
from mcp.server.fastmcp.utilities.types import Image | ||
from mcp.server.fastmcp.utilities.types import Audio, Image | ||
from mcp.server.session import ServerSession | ||
from mcp.shared.exceptions import McpError | ||
from mcp.shared.memory import ( | ||
|
@@ -195,6 +195,10 @@ def image_tool_fn(path: str) -> Image: | |
return Image(path) | ||
|
||
|
||
def audio_tool_fn(path: str) -> Audio: | ||
return Audio(path) | ||
|
||
|
||
def mixed_content_tool_fn() -> list[ContentBlock]: | ||
return [ | ||
TextContent(type="text", text="Hello"), | ||
|
@@ -300,6 +304,60 @@ async def test_tool_image_helper(self, tmp_path: Path): | |
# Check structured content - Image return type should NOT have structured output | ||
assert result.structuredContent is None | ||
|
||
@pytest.mark.anyio | ||
async def test_tool_audio_helper(self, tmp_path: Path): | ||
# Create a test audio | ||
audio_path = tmp_path / "test.wav" | ||
audio_path.write_bytes(b"fake wav data") | ||
|
||
mcp = FastMCP() | ||
mcp.add_tool(audio_tool_fn) | ||
async with client_session(mcp._mcp_server) as client: | ||
result = await client.call_tool("audio_tool_fn", {"path": str(audio_path)}) | ||
assert len(result.content) == 1 | ||
content = result.content[0] | ||
assert isinstance(content, AudioContent) | ||
assert content.type == "audio" | ||
assert content.mimeType == "audio/wav" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you also add something to exercice the suffix-based mime type detection? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just added a test to test the mimetypes, do let me know if this is what you are looking for! |
||
# Verify base64 encoding | ||
decoded = base64.b64decode(content.data) | ||
assert decoded == b"fake wav data" | ||
# Check structured content - Image return type should NOT have structured output | ||
assert result.structuredContent is None | ||
|
||
@pytest.mark.parametrize( | ||
"filename,expected_mime_type", | ||
[ | ||
("test.wav", "audio/wav"), | ||
("test.mp3", "audio/mpeg"), | ||
("test.ogg", "audio/ogg"), | ||
("test.flac", "audio/flac"), | ||
("test.aac", "audio/aac"), | ||
("test.m4a", "audio/mp4"), | ||
("test.unknown", "application/octet-stream"), # Unknown extension fallback | ||
], | ||
) | ||
@pytest.mark.anyio | ||
async def test_tool_audio_suffix_detection(self, tmp_path: Path, filename: str, expected_mime_type: str): | ||
"""Test that Audio helper correctly detects MIME types from file suffixes""" | ||
mcp = FastMCP() | ||
mcp.add_tool(audio_tool_fn) | ||
|
||
# Create a test audio file with the specific extension | ||
audio_path = tmp_path / filename | ||
audio_path.write_bytes(b"fake audio data") | ||
|
||
async with client_session(mcp._mcp_server) as client: | ||
result = await client.call_tool("audio_tool_fn", {"path": str(audio_path)}) | ||
assert len(result.content) == 1 | ||
content = result.content[0] | ||
assert isinstance(content, AudioContent) | ||
assert content.type == "audio" | ||
assert content.mimeType == expected_mime_type | ||
# Verify base64 encoding | ||
decoded = base64.b64decode(content.data) | ||
assert decoded == b"fake audio data" | ||
|
||
@pytest.mark.anyio | ||
async def test_tool_mixed_content(self): | ||
mcp = FastMCP() | ||
|
@@ -332,19 +390,24 @@ async def test_tool_mixed_content(self): | |
assert structured_result[i][key] == value | ||
|
||
@pytest.mark.anyio | ||
async def test_tool_mixed_list_with_image(self, tmp_path: Path): | ||
async def test_tool_mixed_list_with_audio_and_image(self, tmp_path: Path): | ||
"""Test that lists containing Image objects and other types are handled | ||
correctly""" | ||
# Create a test image | ||
image_path = tmp_path / "test.png" | ||
image_path.write_bytes(b"test image data") | ||
|
||
# Create a test audio | ||
audio_path = tmp_path / "test.wav" | ||
audio_path.write_bytes(b"test audio data") | ||
|
||
# TODO(Marcelo): It seems if we add the proper type hint, it generates an invalid JSON schema. | ||
# We need to fix this. | ||
def mixed_list_fn() -> list: # type: ignore | ||
return [ # type: ignore | ||
"text message", | ||
Image(image_path), | ||
Audio(audio_path), | ||
{"key": "value"}, | ||
TextContent(type="text", text="direct content"), | ||
] | ||
|
@@ -353,7 +416,7 @@ def mixed_list_fn() -> list: # type: ignore | |
mcp.add_tool(mixed_list_fn) # type: ignore | ||
async with client_session(mcp._mcp_server) as client: | ||
result = await client.call_tool("mixed_list_fn", {}) | ||
assert len(result.content) == 4 | ||
assert len(result.content) == 5 | ||
# Check text conversion | ||
content1 = result.content[0] | ||
assert isinstance(content1, TextContent) | ||
|
@@ -363,14 +426,19 @@ def mixed_list_fn() -> list: # type: ignore | |
assert isinstance(content2, ImageContent) | ||
assert content2.mimeType == "image/png" | ||
assert base64.b64decode(content2.data) == b"test image data" | ||
# Check dict conversion | ||
# Check audio conversion | ||
content3 = result.content[2] | ||
assert isinstance(content3, TextContent) | ||
assert '"key": "value"' in content3.text | ||
# Check direct TextContent | ||
assert isinstance(content3, AudioContent) | ||
assert content3.mimeType == "audio/wav" | ||
assert base64.b64decode(content3.data) == b"test audio data" | ||
# Check dict conversion | ||
content4 = result.content[3] | ||
assert isinstance(content4, TextContent) | ||
assert content4.text == "direct content" | ||
assert '"key": "value"' in content4.text | ||
# Check direct TextContent | ||
content5 = result.content[4] | ||
assert isinstance(content5, TextContent) | ||
assert content5.text == "direct content" | ||
# Check structured content - untyped list with Image objects should NOT have structured output | ||
assert result.structuredContent is None | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use
@typing.override
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to get some clarity as to what we are tying to override, given the audio class dosent inherit from any class at the moment