{"record":{"id":"9c39c403bc709dc1","repo":"PrefectHQ/fastmcp","slug":"file-not-found-file-path","errorCode":null,"errorMessage":"File not found: {file_path}","messagePattern":"File not found: (.+?)","errorType":"exception","errorClass":"FileNotFoundError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py","lineNumber":89,"sourceCode":"\nclass SkillFileTemplate(ResourceTemplate):\n    \"\"\"A template for accessing files within a skill.\"\"\"\n\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","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py#L71-L107","documentation":"SkillProvider.read() resolves the requested file inside the skill directory and raises FileNotFoundError if the resolved path does not exist on disk. The path has already passed the safe_join security check, so this is purely a missing-file condition reported with the caller-supplied file_path.","triggerScenarios":"read() (via _read) with a file_path that survives path validation but doesn't exist under the skill directory — wrong filename, wrong case, deleted/renamed file, or file not shipped with the skill.","commonSituations":"Typo'd filenames, case-sensitive filesystems (Linux) vs case-insensitive expectations, skill folders missing optional assets, stale URIs after skill reorganization.","solutions":["List the skill's files (e.g. via its resources) and read an existing filename with correct casing","Fix the path in the caller/URI to match the actual file on disk","Ship or restore the missing file into the skill directory"],"exampleFix":"// before\nawait provider.read('skill://my-skill/SKIL.MD')  # wrong case\n// after\nawait provider.read('skill://my-skill/SKILL.md')","handlingStrategy":"fallback","validationCode":"from pathlib import Path\ntarget = Path(skill_dir) / file_path\nif not target.is_file():\n    file_path = suggest_closest_file(skill_dir, file_path)  # fuzzy-match or list files","typeGuard":null,"tryCatchPattern":"try:\n    content = await provider.read(uri)\nexcept FileNotFoundError as e:\n    logger.warning('Missing skill file: %s', e)\n    content = await provider.read(default_uri_for(skill))","preventionTips":["List the skill's files before reading rather than hard-coding names","Match filename casing exactly; assume case-sensitive filesystems","Include all referenced assets when packaging skills; add existence checks to skill CI"],"tags":["file-not-found","skill-provider","filesystem"],"backgroundTag":"file-not-found","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}