Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/mcp/server/fastmcp/prompts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import pydantic_core
from pydantic import BaseModel, Field, TypeAdapter, validate_call

from mcp.types import EmbeddedResource, ImageContent, TextContent
from mcp.types import EmbeddedResource, ImageContent, TextContent, FileContent

CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource | FileContent


class Message(BaseModel):
Expand Down
7 changes: 4 additions & 3 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
ImageContent,
TextContent,
ToolAnnotations,
FileContent,
)
from mcp.types import Prompt as MCPPrompt
from mcp.types import PromptArgument as MCPPromptArgument
Expand Down Expand Up @@ -273,7 +274,7 @@ def get_context(self) -> Context[ServerSession, object]:

async def call_tool(
self, name: str, arguments: dict[str, Any]
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
) -> Sequence[TextContent | ImageContent | EmbeddedResource | FileContent]:
"""Call a tool by name with arguments."""
context = self.get_context()
result = await self._tool_manager.call_tool(name, arguments, context=context)
Expand Down Expand Up @@ -873,12 +874,12 @@ async def get_prompt(

def _convert_to_content(
result: Any,
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
) -> Sequence[TextContent | ImageContent | EmbeddedResource | FileContent]:
"""Convert a result to a sequence of content objects."""
if result is None:
return []

if isinstance(result, TextContent | ImageContent | EmbeddedResource):
if isinstance(result, TextContent | ImageContent | EmbeddedResource | FileContent):
return [result]

if isinstance(result, Image):
Expand Down
5 changes: 4 additions & 1 deletion src/mcp/server/lowlevel/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,10 @@ def decorator(
...,
Awaitable[
Iterable[
types.TextContent | types.ImageContent | types.EmbeddedResource
types.TextContent
| types.ImageContent
| types.EmbeddedResource
| types.FileContent
]
],
],
Expand Down
17 changes: 16 additions & 1 deletion src/mcp/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,21 @@ class ImageContent(BaseModel):
model_config = ConfigDict(extra="allow")


class FileContent(BaseModel):
"""File content for a message."""

type: Literal["file"]
filename: str
data: str
"""The base64-encoded file data."""
mimeType: str
"""
The file mimetype
"""
annotations: Annotations | None = None
model_config = ConfigDict(extra="allow")


class SamplingMessage(BaseModel):
"""Describes a message issued to or received from an LLM API."""

Expand Down Expand Up @@ -797,7 +812,7 @@ class CallToolRequest(Request[CallToolRequestParams, Literal["tools/call"]]):
class CallToolResult(Result):
"""The server's response to a tool call."""

content: list[TextContent | ImageContent | EmbeddedResource]
content: list[TextContent | ImageContent | EmbeddedResource | FileContent]
isError: bool = False


Expand Down
35 changes: 17 additions & 18 deletions tests/server/fastmcp/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from mcp.types import (
BlobResourceContents,
ImageContent,
FileContent,
TextContent,
TextResourceContents,
)
Expand Down Expand Up @@ -59,7 +60,7 @@ async def test_sse_app_with_mount_path(self):
# Test with default mount path
mcp = FastMCP()
with patch.object(
mcp, "_normalize_path", return_value="/messages/"
mcp, "_normalize_path", return_value="/messages/"
) as mock_normalize:
mcp.sse_app()
# Verify _normalize_path was called with correct args
Expand All @@ -69,7 +70,7 @@ async def test_sse_app_with_mount_path(self):
mcp = FastMCP()
mcp.settings.mount_path = "/custom"
with patch.object(
mcp, "_normalize_path", return_value="/custom/messages/"
mcp, "_normalize_path", return_value="/custom/messages/"
) as mock_normalize:
mcp.sse_app()
# Verify _normalize_path was called with correct args
Expand All @@ -78,7 +79,7 @@ async def test_sse_app_with_mount_path(self):
# Test with mount_path parameter
mcp = FastMCP()
with patch.object(
mcp, "_normalize_path", return_value="/param/messages/"
mcp, "_normalize_path", return_value="/param/messages/"
) as mock_normalize:
mcp.sse_app(mount_path="/param")
# Verify _normalize_path was called with correct args
Expand All @@ -103,7 +104,7 @@ async def test_starlette_routes_with_mount_path(self):
# Verify path values
assert sse_routes[0].path == "/sse", "SSE route path should be /sse"
assert (
mount_routes[0].path == "/messages"
mount_routes[0].path == "/messages"
), "Mount route path should be /messages"

# Test with mount path as parameter
Expand All @@ -121,7 +122,7 @@ async def test_starlette_routes_with_mount_path(self):
# Verify path values
assert sse_routes[0].path == "/sse", "SSE route path should be /sse"
assert (
mount_routes[0].path == "/messages"
mount_routes[0].path == "/messages"
), "Mount route path should be /messages"

@pytest.mark.anyio
Expand All @@ -131,7 +132,7 @@ async def test_non_ascii_description(self):

@mcp.tool(
description=(
"🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉"
"🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉"
)
)
def hello_world(name: str = "世界") -> str:
Expand Down Expand Up @@ -167,7 +168,6 @@ async def test_add_tool_decorator_incorrect_usage(self):
mcp = FastMCP()

with pytest.raises(TypeError, match="The @tool decorator was used incorrectly"):

@mcp.tool # Missing parentheses #type: ignore
def add(x: int, y: int) -> int:
return x + y
Expand All @@ -187,9 +187,8 @@ async def test_add_resource_decorator_incorrect_usage(self):
mcp = FastMCP()

with pytest.raises(
TypeError, match="The @resource decorator was used incorrectly"
TypeError, match="The @resource decorator was used incorrectly"
):

@mcp.resource # Missing parentheses #type: ignore
def get_data(x: str) -> str:
return f"Data: {x}"
Expand All @@ -207,10 +206,11 @@ def image_tool_fn(path: str) -> Image:
return Image(path)


def mixed_content_tool_fn() -> list[TextContent | ImageContent]:
def mixed_content_tool_fn() -> list[TextContent | ImageContent | FileContent]:
return [
TextContent(type="text", text="Hello"),
ImageContent(type="image", data="abc", mimeType="image/png"),
FileContent(type="file", data="abc", filename="test.pdf", mimeType="application/pdf"),
]


Expand Down Expand Up @@ -312,14 +312,18 @@ async def test_tool_mixed_content(self):
mcp.add_tool(mixed_content_tool_fn)
async with client_session(mcp._mcp_server) as client:
result = await client.call_tool("mixed_content_tool_fn", {})
assert len(result.content) == 2
assert len(result.content) == 3
content1 = result.content[0]
content2 = result.content[1]
content3 = result.content[2]
assert isinstance(content1, TextContent)
assert content1.text == "Hello"
assert isinstance(content2, ImageContent)
assert content2.mimeType == "image/png"
assert content2.data == "abc"
assert isinstance(content3, FileContent)
assert content3.mimeType == "application/pdf"
assert content3.data == "abc"

@pytest.mark.anyio
async def test_tool_mixed_list_with_image(self, tmp_path: Path):
Expand Down Expand Up @@ -437,8 +441,8 @@ async def test_file_resource_binary(self, tmp_path: Path):
result = await client.read_resource(AnyUrl("file://test.bin"))
assert isinstance(result.contents[0], BlobResourceContents)
assert (
result.contents[0].blob
== base64.b64encode(b"Binary file data").decode()
result.contents[0].blob
== base64.b64encode(b"Binary file data").decode()
)

@pytest.mark.anyio
Expand Down Expand Up @@ -468,7 +472,6 @@ async def test_resource_with_params(self):
mcp = FastMCP()

with pytest.raises(ValueError, match="Mismatch between URI parameters"):

@mcp.resource("resource://data")
def get_data_fn(param: str) -> str:
return f"Data: {param}"
Expand All @@ -479,7 +482,6 @@ async def test_resource_with_uri_params(self):
mcp = FastMCP()

with pytest.raises(ValueError, match="Mismatch between URI parameters"):

@mcp.resource("resource://{param}")
def get_data() -> str:
return "Data"
Expand Down Expand Up @@ -513,7 +515,6 @@ async def test_resource_mismatched_params(self):
mcp = FastMCP()

with pytest.raises(ValueError, match="Mismatch between URI parameters"):

@mcp.resource("resource://{name}/data")
def get_data(user: str) -> str:
return f"Data for {user}"
Expand All @@ -540,7 +541,6 @@ async def test_resource_multiple_mismatched_params(self):
mcp = FastMCP()

with pytest.raises(ValueError, match="Mismatch between URI parameters"):

@mcp.resource("resource://{org}/{repo}/data")
def get_data_mismatched(org: str, repo_2: str) -> str:
return f"Data for {org}"
Expand Down Expand Up @@ -774,7 +774,6 @@ def test_prompt_decorator_error(self):
"""Test error when decorator is used incorrectly."""
mcp = FastMCP()
with pytest.raises(TypeError, match="decorator was used incorrectly"):

@mcp.prompt # type: ignore
def fn() -> str:
return "Hello, world!"
Expand Down
Loading