fix: respect resource mime type in responses

The server was ignoring mime types set on resources, defaulting to text/plain
for strings and application/octet-stream for bytes. Now properly preserves
the specified mime type in both FastMCP and low-level server implementations.

Note that this is breaks backwards compatibility as it changes the return
values of read_resource() on FastMCP. It is BC compatible on lowlevel since
it only extends the callback.

Github-Issue: #152
Reported-by: eiseleMichael
This commit is contained in:
David Soria Parra
2025-01-24 14:20:42 +00:00
parent f10c2e3f1f
commit 8ff4b5e9d3
6 changed files with 82 additions and 40 deletions

View File

@@ -197,14 +197,16 @@ class FastMCP:
for template in templates
]
async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
async def read_resource(self, uri: AnyUrl | str) -> tuple[str | bytes, str]:
"""Read a resource by URI."""
resource = await self._resource_manager.get_resource(uri)
if not resource:
raise ResourceError(f"Unknown resource: {uri}")
try:
return await resource.read()
content = await resource.read()
return (content, resource.mime_type)
except Exception as e:
logger.error(f"Error reading resource {uri}: {e}")
raise ResourceError(str(e))
@@ -606,7 +608,7 @@ class Context(BaseModel):
progress_token=progress_token, progress=progress, total=total
)
async def read_resource(self, uri: str | AnyUrl) -> str | bytes:
async def read_resource(self, uri: str | AnyUrl) -> tuple[str | bytes, str]:
"""Read a resource by URI.
Args:

View File

@@ -252,32 +252,55 @@ class Server:
return decorator
def read_resource(self):
def decorator(func: Callable[[AnyUrl], Awaitable[str | bytes]]):
def decorator(
func: Callable[[AnyUrl], Awaitable[str | bytes | tuple[str | bytes, str]]],
):
logger.debug("Registering handler for ReadResourceRequest")
async def handler(req: types.ReadResourceRequest):
result = await func(req.params.uri)
def create_content(data: str | bytes, mime_type: str):
match data:
case str() as data:
return types.TextResourceContents(
uri=req.params.uri,
text=data,
mimeType=mime_type,
)
case bytes() as data:
import base64
return types.BlobResourceContents(
uri=req.params.uri,
blob=base64.urlsafe_b64encode(data).decode(),
mimeType=mime_type,
)
match result:
case str(s):
content = types.TextResourceContents(
uri=req.params.uri,
text=s,
mimeType="text/plain",
case str() | bytes() as data:
default_mime = (
"text/plain"
if isinstance(data, str)
else "application/octet-stream"
)
case bytes(b):
import base64
content = types.BlobResourceContents(
uri=req.params.uri,
blob=base64.urlsafe_b64encode(b).decode(),
mimeType="application/octet-stream",
content = create_content(data, default_mime)
return types.ServerResult(
types.ReadResourceResult(
contents=[content],
)
)
case (data, mime_type):
content = create_content(data, mime_type)
return types.ServerResult(
types.ReadResourceResult(
contents=[content],
)
)
case _:
raise ValueError(
f"Unexpected return type from read_resource: {type(result)}"
)
return types.ServerResult(
types.ReadResourceResult(
contents=[content],
)
)
self.request_handlers[types.ReadResourceRequest] = handler
return func