PrefectHQ/fastmcp · error · ValueError
Not a file: {file_path}
Error message
Not a file: {file_path} What it means
SkillProvider.read() requires the resolved path to be a regular file. If the path exists but is a directory (or other non-regular file), it raises ValueError('Not a file: ...'). This prevents attempting read_text/read_bytes on directories.
Source
Thrown at fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py:92
skill_info: SkillInfo
async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:
"""Read a file from the skill directory."""
file_path = arguments.get("path", "")
# Security: reject traversal, absolute-path injection, null bytes, and
# symlink escapes before touching the filesystem.
try:
full_path = safe_join(self.skill_info.path, file_path)
except PathEscapeError as e:
raise ValueError(f"Invalid path: {e}") from e
if not full_path.exists():
raise FileNotFoundError(f"File not found: {file_path}")
if not full_path.is_file():
raise ValueError(f"Not a file: {file_path}")
# Determine if binary or text based on mime type
mime_type, _ = mimetypes.guess_type(str(full_path))
if mime_type and mime_type.startswith("text/"):
return full_path.read_text(encoding="utf-8")
else:
return full_path.read_bytes()
async def _read(
self,
uri: str,
params: dict[str, Any],
) -> ResourceResult:
"""Server entry point - read file directly without creating ephemeral resource."""
# Call read() directly and convert to ResourceResult
result = await self.read(arguments=params)
return self.convert_result(result)
View on GitHub (pinned to 1f02114297)
Solutions
- Pass the full path to a specific file, not a directory
- If you need multiple files, enumerate the directory contents and read each file individually
- Verify with a local os.path.isfile check against the skill directory before constructing the URI
Example fix
// before
await provider.read('skill://my-skill/scripts') # a directory
// after
await provider.read('skill://my-skill/scripts/run.py') Defensive patterns
Strategy: validation
Validate before calling
import os
target = os.path.join(skill_dir, file_path)
if os.path.isdir(target):
raise ValueError(f'{file_path} is a directory; pass a specific file') Type guard
import os
def is_regular_file_under(skill_dir: str, rel: str) -> bool:
return os.path.isfile(os.path.join(skill_dir, rel)) Try / catch
try:
content = await provider.read(uri)
except ValueError as e:
if 'Not a file' in str(e):
files = list_files_in(uri_dir(uri))
content = await provider.read(choose_file(files)) Prevention
- Always point URIs at a concrete file, never a directory
- Enumerate directory contents and read files individually
- Add a pre-read isfile assertion when paths are computed programmatically
When it happens
Trigger: read() (via _read) with a file_path that resolves to a directory inside the skill folder, or a special file (socket, fifo) rather than a regular file.
Common situations: Requesting the skill root or a subdirectory name instead of a specific file; URIs built by omitting the filename component; assuming a directory of assets can be read as one resource.
Related errors
- Path must be absolute
- Invalid path: {e}
- File not found: {file_path}
- Expected integer, got {raw!r}
- Expected number, got {raw!r}
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/6fcc6fc3d98a4507.
Report an issue: GitHub.