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

  1. Pass the full path to a specific file, not a directory
  2. If you need multiple files, enumerate the directory contents and read each file individually
  3. 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

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


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/6fcc6fc3d98a4507. Report an issue: GitHub.