mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-26 15:34:18 +02:00
Async FileResource and DirectoryResource (#2241)
* Improve DirectoryResource exception logging and async implementation - Add exception logging before raising ResourceError in read() method - Convert list_files() to async-native using anyio.Path - Update read() to await async list_files() and use async is_file() check - Remove synchronous thread wrapper in favor of native async I/O Co-authored-by: William Easton <strawgate@users.noreply.github.com> * Clean-up DirectoryResource * Update src/fastmcp/resources/types.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: William Easton <strawgate@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
1e5776f69c
commit
380835593c
1 changed files with 30 additions and 24 deletions
|
|
@ -5,11 +5,11 @@ from __future__ import annotations
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
import anyio.to_thread
|
||||
import httpx
|
||||
import pydantic.json
|
||||
from anyio import Path as AsyncPath
|
||||
from pydantic import Field, ValidationInfo
|
||||
from typing_extensions import override
|
||||
|
||||
from fastmcp.exceptions import ResourceError
|
||||
from fastmcp.resources.resource import Resource
|
||||
|
|
@ -54,6 +54,10 @@ class FileResource(Resource):
|
|||
description="MIME type of the resource content",
|
||||
)
|
||||
|
||||
@property
|
||||
def _async_path(self) -> AsyncPath:
|
||||
return AsyncPath(self.path)
|
||||
|
||||
@pydantic.field_validator("path")
|
||||
@classmethod
|
||||
def validate_absolute_path(cls, path: Path) -> Path:
|
||||
|
|
@ -71,12 +75,13 @@ class FileResource(Resource):
|
|||
mime_type = info.data.get("mime_type", "text/plain")
|
||||
return not mime_type.startswith("text/")
|
||||
|
||||
@override
|
||||
async def read(self) -> str | bytes:
|
||||
"""Read the file content."""
|
||||
try:
|
||||
if self.is_binary:
|
||||
return await anyio.to_thread.run_sync(self.path.read_bytes)
|
||||
return await anyio.to_thread.run_sync(self.path.read_text)
|
||||
return await self._async_path.read_bytes()
|
||||
return await self._async_path.read_text()
|
||||
except Exception as e:
|
||||
raise ResourceError(f"Error reading file {self.path}") from e
|
||||
|
||||
|
|
@ -89,11 +94,12 @@ class HttpResource(Resource):
|
|||
default="application/json", description="MIME type of the resource content"
|
||||
)
|
||||
|
||||
@override
|
||||
async def read(self) -> str | bytes:
|
||||
"""Read the HTTP content."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(self.url)
|
||||
response.raise_for_status()
|
||||
_ = response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
|
||||
|
|
@ -111,6 +117,10 @@ class DirectoryResource(Resource):
|
|||
default="application/json", description="MIME type of the resource content"
|
||||
)
|
||||
|
||||
@property
|
||||
def _async_path(self) -> AsyncPath:
|
||||
return AsyncPath(self.path)
|
||||
|
||||
@pydantic.field_validator("path")
|
||||
@classmethod
|
||||
def validate_absolute_path(cls, path: Path) -> Path:
|
||||
|
|
@ -119,33 +129,29 @@ class DirectoryResource(Resource):
|
|||
raise ValueError("Path must be absolute")
|
||||
return path
|
||||
|
||||
def list_files(self) -> list[Path]:
|
||||
async def list_files(self) -> list[Path]:
|
||||
"""List files in the directory."""
|
||||
if not self.path.exists():
|
||||
if not await self._async_path.exists():
|
||||
raise FileNotFoundError(f"Directory not found: {self.path}")
|
||||
if not self.path.is_dir():
|
||||
if not await self._async_path.is_dir():
|
||||
raise NotADirectoryError(f"Not a directory: {self.path}")
|
||||
|
||||
try:
|
||||
if self.pattern:
|
||||
return (
|
||||
list(self.path.glob(self.pattern))
|
||||
if not self.recursive
|
||||
else list(self.path.rglob(self.pattern))
|
||||
)
|
||||
return (
|
||||
list(self.path.glob("*"))
|
||||
if not self.recursive
|
||||
else list(self.path.rglob("*"))
|
||||
)
|
||||
except Exception as e:
|
||||
raise ResourceError(f"Error listing directory {self.path}: {e}")
|
||||
pattern = self.pattern or "*"
|
||||
|
||||
glob_fn = self._async_path.rglob if self.recursive else self._async_path.glob
|
||||
try:
|
||||
return [Path(p) async for p in glob_fn(pattern) if await p.is_file()]
|
||||
except Exception as e:
|
||||
raise ResourceError(f"Error listing directory {self.path}") from e
|
||||
|
||||
@override
|
||||
async def read(self) -> str: # Always returns JSON string
|
||||
"""Read the directory listing."""
|
||||
try:
|
||||
files = await anyio.to_thread.run_sync(self.list_files)
|
||||
file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]
|
||||
files: list[Path] = await self.list_files()
|
||||
|
||||
file_list = [str(f.relative_to(self.path)) for f in files]
|
||||
|
||||
return json.dumps({"files": file_list}, indent=2)
|
||||
except Exception:
|
||||
raise ResourceError(f"Error reading directory {self.path}")
|
||||
except Exception as e:
|
||||
raise ResourceError(f"Error reading directory {self.path}") from e
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue