Skip to content

Commit f3b948e

Browse files
committed
feat: support audio content
Adds support for a distinct AudioContent type as defined in the specification. This happens to share a structure with ImageContent, but should probably be distinguished for parity with the specification itself.
1 parent 6353dd1 commit f3b948e

File tree

9 files changed

+60
-19
lines changed

9 files changed

+60
-19
lines changed

examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ def main(
4343
@app.call_tool()
4444
async def call_tool(
4545
name: str, arguments: dict
46-
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
46+
) -> list[
47+
types.TextContent
48+
| types.ImageContent
49+
| types.AudioContent
50+
| types.EmbeddedResource
51+
]:
4752
ctx = app.request_context
4853
interval = arguments.get("interval", 1.0)
4954
count = arguments.get("count", 5)

examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ def main(
4747
@app.call_tool()
4848
async def call_tool(
4949
name: str, arguments: dict
50-
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
50+
) -> list[
51+
types.TextContent
52+
| types.ImageContent
53+
| types.AudioContent
54+
| types.EmbeddedResource
55+
]:
5156
ctx = app.request_context
5257
interval = arguments.get("interval", 1.0)
5358
count = arguments.get("count", 5)

examples/servers/simple-tool/mcp_simple_tool/server.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
async def fetch_website(
99
url: str,
10-
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
10+
) -> list[
11+
types.TextContent | types.ImageContent | types.AudioContent | types.EmbeddedResource
12+
]:
1113
headers = {
1214
"User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)"
1315
}
@@ -31,7 +33,12 @@ def main(port: int, transport: str) -> int:
3133
@app.call_tool()
3234
async def fetch_tool(
3335
name: str, arguments: dict
34-
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
36+
) -> list[
37+
types.TextContent
38+
| types.ImageContent
39+
| types.AudioContent
40+
| types.EmbeddedResource
41+
]:
3542
if name != "fetch":
3643
raise ValueError(f"Unknown tool: {name}")
3744
if "url" not in arguments:

src/mcp/server/fastmcp/prompts/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import pydantic_core
88
from pydantic import BaseModel, Field, TypeAdapter, validate_call
99

10-
from mcp.types import EmbeddedResource, ImageContent, TextContent
10+
from mcp.types import AudioContent, EmbeddedResource, ImageContent, TextContent
1111

12-
CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
12+
CONTENT_TYPES = TextContent | ImageContent | AudioContent | EmbeddedResource
1313

1414

1515
class Message(BaseModel):

src/mcp/server/fastmcp/server.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from mcp.shared.context import LifespanContextT, RequestContext
5353
from mcp.types import (
5454
AnyFunction,
55+
AudioContent,
5556
EmbeddedResource,
5657
GetPromptResult,
5758
ImageContent,
@@ -271,7 +272,7 @@ def get_context(self) -> Context[ServerSession, object]:
271272

272273
async def call_tool(
273274
self, name: str, arguments: dict[str, Any]
274-
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
275+
) -> Sequence[TextContent | ImageContent | AudioContent | EmbeddedResource]:
275276
"""Call a tool by name with arguments."""
276277
context = self.get_context()
277278
result = await self._tool_manager.call_tool(name, arguments, context=context)
@@ -871,12 +872,12 @@ async def get_prompt(
871872

872873
def _convert_to_content(
873874
result: Any,
874-
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
875+
) -> Sequence[TextContent | ImageContent | AudioContent | EmbeddedResource]:
875876
"""Convert a result to a sequence of content objects."""
876877
if result is None:
877878
return []
878879

879-
if isinstance(result, TextContent | ImageContent | EmbeddedResource):
880+
if isinstance(result, TextContent | ImageContent | AudioContent | EmbeddedResource):
880881
return [result]
881882

882883
if isinstance(result, Image):

src/mcp/server/lowlevel/server.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,10 @@ def decorator(
400400
...,
401401
Awaitable[
402402
Iterable[
403-
types.TextContent | types.ImageContent | types.EmbeddedResource
403+
types.TextContent
404+
| types.ImageContent
405+
| types.AudioContent
406+
| types.EmbeddedResource
404407
]
405408
],
406409
],

src/mcp/types.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -651,11 +651,26 @@ class ImageContent(BaseModel):
651651
model_config = ConfigDict(extra="allow")
652652

653653

654+
class AudioContent(BaseModel):
655+
"""Audio content for a message."""
656+
657+
type: Literal["audio"]
658+
data: str
659+
"""The base64-encoded audio data."""
660+
mimeType: str
661+
"""
662+
The MIME type of the audio. Different providers may support different
663+
audio types.
664+
"""
665+
annotations: Annotations | None = None
666+
model_config = ConfigDict(extra="allow")
667+
668+
654669
class SamplingMessage(BaseModel):
655670
"""Describes a message issued to or received from an LLM API."""
656671

657672
role: Role
658-
content: TextContent | ImageContent
673+
content: TextContent | ImageContent | AudioContent
659674
model_config = ConfigDict(extra="allow")
660675

661676

@@ -677,7 +692,7 @@ class PromptMessage(BaseModel):
677692
"""Describes a message returned as part of a prompt."""
678693

679694
role: Role
680-
content: TextContent | ImageContent | EmbeddedResource
695+
content: TextContent | ImageContent | AudioContent | EmbeddedResource
681696
model_config = ConfigDict(extra="allow")
682697

683698

@@ -796,7 +811,7 @@ class CallToolRequest(Request[CallToolRequestParams, Literal["tools/call"]]):
796811
class CallToolResult(Result):
797812
"""The server's response to a tool call."""
798813

799-
content: list[TextContent | ImageContent | EmbeddedResource]
814+
content: list[TextContent | ImageContent | AudioContent | EmbeddedResource]
800815
isError: bool = False
801816

802817

@@ -960,7 +975,7 @@ class CreateMessageResult(Result):
960975
"""The client's response to a sampling/create_message request from the server."""
961976

962977
role: Role
963-
content: TextContent | ImageContent
978+
content: TextContent | ImageContent | AudioContent
964979
model: str
965980
"""The name of the model that generated the message."""
966981
stopReason: StopReason | None = None

tests/issues/test_88_random_error.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from mcp.server.lowlevel import Server
1313
from mcp.shared.exceptions import McpError
1414
from mcp.types import (
15+
AudioContent,
1516
EmbeddedResource,
1617
ImageContent,
1718
TextContent,
@@ -37,7 +38,7 @@ async def test_notification_validation_error(tmp_path: Path):
3738
@server.call_tool()
3839
async def slow_tool(
3940
name: str, arg
40-
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
41+
) -> Sequence[TextContent | ImageContent | AudioContent | EmbeddedResource]:
4142
nonlocal request_count
4243
request_count += 1
4344

tests/server/fastmcp/test_server.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
create_connected_server_and_client_session as client_session,
1717
)
1818
from mcp.types import (
19+
AudioContent,
1920
BlobResourceContents,
2021
ImageContent,
2122
TextContent,
@@ -207,10 +208,11 @@ def image_tool_fn(path: str) -> Image:
207208
return Image(path)
208209

209210

210-
def mixed_content_tool_fn() -> list[TextContent | ImageContent]:
211+
def mixed_content_tool_fn() -> list[TextContent | ImageContent | AudioContent]:
211212
return [
212213
TextContent(type="text", text="Hello"),
213214
ImageContent(type="image", data="abc", mimeType="image/png"),
215+
AudioContent(type="audio", data="def", mimeType="audio/wav"),
214216
]
215217

216218

@@ -312,14 +314,16 @@ async def test_tool_mixed_content(self):
312314
mcp.add_tool(mixed_content_tool_fn)
313315
async with client_session(mcp._mcp_server) as client:
314316
result = await client.call_tool("mixed_content_tool_fn", {})
315-
assert len(result.content) == 2
316-
content1 = result.content[0]
317-
content2 = result.content[1]
317+
assert len(result.content) == 3
318+
content1, content2, content3 = result.content
318319
assert isinstance(content1, TextContent)
319320
assert content1.text == "Hello"
320321
assert isinstance(content2, ImageContent)
321322
assert content2.mimeType == "image/png"
322323
assert content2.data == "abc"
324+
assert isinstance(content3, AudioContent)
325+
assert content3.mimeType == "audio/wav"
326+
assert content3.data == "def"
323327

324328
@pytest.mark.anyio
325329
async def test_tool_mixed_list_with_image(self, tmp_path: Path):

0 commit comments

Comments
 (0)