{"record":{"id":"6fcc6fc3d98a4507","repo":"PrefectHQ/fastmcp","slug":"not-a-file-file-path","errorCode":null,"errorMessage":"Not a file: {file_path}","messagePattern":"Not a file: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py","lineNumber":92,"sourceCode":"\n    skill_info: SkillInfo\n\n    async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:\n        \"\"\"Read a file from the skill directory.\"\"\"\n        file_path = arguments.get(\"path\", \"\")\n\n        # Security: reject traversal, absolute-path injection, null bytes, and\n        # symlink escapes before touching the filesystem.\n        try:\n            full_path = safe_join(self.skill_info.path, file_path)\n        except PathEscapeError as e:\n            raise ValueError(f\"Invalid path: {e}\") from e\n\n        if not full_path.exists():\n            raise FileNotFoundError(f\"File not found: {file_path}\")\n\n        if not full_path.is_file():\n            raise ValueError(f\"Not a file: {file_path}\")\n\n        # Determine if binary or text based on mime type\n        mime_type, _ = mimetypes.guess_type(str(full_path))\n        if mime_type and mime_type.startswith(\"text/\"):\n            return full_path.read_text(encoding=\"utf-8\")\n        else:\n            return full_path.read_bytes()\n\n    async def _read(\n        self,\n        uri: str,\n        params: dict[str, Any],\n    ) -> ResourceResult:\n        \"\"\"Server entry point - read file directly without creating ephemeral resource.\"\"\"\n        # Call read() directly and convert to ResourceResult\n        result = await self.read(arguments=params)\n        return self.convert_result(result)\n","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py#L74-L110","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait provider.read('skill://my-skill/scripts')  # a directory\n// after\nawait provider.read('skill://my-skill/scripts/run.py')","handlingStrategy":"validation","validationCode":"import os\ntarget = os.path.join(skill_dir, file_path)\nif os.path.isdir(target):\n    raise ValueError(f'{file_path} is a directory; pass a specific file')","typeGuard":"import os\ndef is_regular_file_under(skill_dir: str, rel: str) -> bool:\n    return os.path.isfile(os.path.join(skill_dir, rel))","tryCatchPattern":"try:\n    content = await provider.read(uri)\nexcept ValueError as e:\n    if 'Not a file' in str(e):\n        files = list_files_in(uri_dir(uri))\n        content = await provider.read(choose_file(files))","preventionTips":["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"],"tags":["not-a-file","skill-provider","filesystem","validation"],"backgroundTag":"not-a-file","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}