ha-mcp-dev 7.14.2.dev2095__tar.gz → 7.14.2.dev2098__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ha_mcp_dev-7.14.2.dev2095/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.14.2.dev2098}/PKG-INFO +1 -1
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/pyproject.toml +1 -1
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/backup_manager.py +53 -22
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_fs.py +20 -2
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/backup.py +5 -2
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/helpers.py +35 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_bug_report.py +117 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_filesystem.py +79 -38
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/LICENSE +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/README.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/setup.cfg +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.14.2.
|
|
7
|
+
version = "7.14.2.dev2098"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# Upper bound widened to <3.15 so the package installs on Home Assistant's
|
|
@@ -766,16 +766,18 @@ class BackupManager:
|
|
|
766
766
|
|
|
767
767
|
# ----- legacy store (pre-#1579 .ha_mcp_tools_backups/) ---------------
|
|
768
768
|
|
|
769
|
-
async def list_legacy(self) -> list[dict[str, Any]]:
|
|
769
|
+
async def list_legacy(self) -> tuple[list[dict[str, Any]], str | None]:
|
|
770
770
|
"""List pre-#1579 ``.bak`` backups via the component service.
|
|
771
771
|
|
|
772
772
|
Each entry is normalized to a synthetic ``name`` (``legacy:<file>``)
|
|
773
773
|
plus ``source="legacy"`` and the decode hints (``file_path`` /
|
|
774
774
|
``path_ambiguous``) so the caller can route view/diff/restore and warn
|
|
775
|
-
on un-restorable (ambiguous) names. Returns ``
|
|
776
|
-
|
|
775
|
+
on un-restorable (ambiguous) names. Returns ``(entries,
|
|
776
|
+
unavailable_reason)``: entries is ``[]`` when the component is too
|
|
777
|
+
old, unconfigured, or absent — ``list`` still works — and the reason
|
|
778
|
+
(when set) lets the caller flag the omission.
|
|
777
779
|
"""
|
|
778
|
-
backups = await _list_legacy_backups(self._client)
|
|
780
|
+
backups, unavailable_reason = await _list_legacy_backups(self._client)
|
|
779
781
|
out: list[dict[str, Any]] = []
|
|
780
782
|
for b in backups:
|
|
781
783
|
filename = b.get("filename")
|
|
@@ -792,7 +794,7 @@ class BackupManager:
|
|
|
792
794
|
"path_ambiguous": b.get("path_ambiguous", True),
|
|
793
795
|
}
|
|
794
796
|
)
|
|
795
|
-
return out
|
|
797
|
+
return out, unavailable_reason
|
|
796
798
|
|
|
797
799
|
async def read_legacy(self, filename: str) -> dict[str, Any]:
|
|
798
800
|
"""Read one legacy ``.bak`` (raw content + decode hints).
|
|
@@ -815,7 +817,7 @@ class BackupManager:
|
|
|
815
817
|
domain: str | None = None,
|
|
816
818
|
entity_id: str | None = None,
|
|
817
819
|
limit: int | None = None,
|
|
818
|
-
) -> list[dict[str, Any]]:
|
|
820
|
+
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
819
821
|
"""Edits-store snapshots plus pre-#1579 legacy ``.bak`` entries (#1579).
|
|
820
822
|
|
|
821
823
|
``list_snapshots`` is sync (dir glob, run off-thread); the legacy store
|
|
@@ -828,9 +830,22 @@ class BackupManager:
|
|
|
828
830
|
Legacy entries get reserved room within ``limit`` so a full edits store
|
|
829
831
|
(>= ``limit`` snapshots) can't truncate the few historical legacy
|
|
830
832
|
entries out of the listing — surfacing them is the whole point.
|
|
833
|
+
|
|
834
|
+
Returns ``(entries, warnings)``: when the legacy sub-fetch was
|
|
835
|
+
suppressed (tools entry not set up / component too old), ``warnings``
|
|
836
|
+
says so, so the tool response doesn't read as a clean, complete list
|
|
837
|
+
with the ``.bak`` history silently missing (#1996).
|
|
831
838
|
"""
|
|
832
839
|
want_legacy = domain in (None, "yaml_file") and entity_id is None
|
|
833
|
-
legacy
|
|
840
|
+
legacy: list[dict[str, Any]] = []
|
|
841
|
+
warnings: list[str] = []
|
|
842
|
+
if want_legacy:
|
|
843
|
+
legacy, unavailable_reason = await self.list_legacy()
|
|
844
|
+
if unavailable_reason:
|
|
845
|
+
warnings.append(
|
|
846
|
+
"Legacy .bak backups could not be listed and are omitted: "
|
|
847
|
+
f"{unavailable_reason}"
|
|
848
|
+
)
|
|
834
849
|
edits_limit = max(1, limit - len(legacy)) if limit and legacy else limit
|
|
835
850
|
entries = await asyncio.to_thread(
|
|
836
851
|
self.list_snapshots, domain=domain, entity_id=entity_id, limit=edits_limit
|
|
@@ -838,7 +853,7 @@ class BackupManager:
|
|
|
838
853
|
entries.extend(legacy)
|
|
839
854
|
if limit:
|
|
840
855
|
entries = entries[:limit]
|
|
841
|
-
return entries
|
|
856
|
+
return entries, warnings
|
|
842
857
|
|
|
843
858
|
async def _diff_legacy(self, filename: str) -> DiffResponseText:
|
|
844
859
|
info = await self.read_legacy(filename)
|
|
@@ -2173,30 +2188,37 @@ async def _restore_yaml_file(client: Any, entity_id: str, config: Any) -> Any:
|
|
|
2173
2188
|
return result
|
|
2174
2189
|
|
|
2175
2190
|
|
|
2176
|
-
async def _list_legacy_backups(client: Any) -> list[dict[str, Any]]:
|
|
2191
|
+
async def _list_legacy_backups(client: Any) -> tuple[list[dict[str, Any]], str | None]:
|
|
2177
2192
|
"""Fetch pre-#1579 ``.bak`` backups via the component list_legacy_backups
|
|
2178
2193
|
service.
|
|
2179
2194
|
|
|
2180
|
-
Returns ``
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2195
|
+
Returns ``(backups, unavailable_reason)``. The legacy store is
|
|
2196
|
+
best-effort: when the component predates the service, isn't configured
|
|
2197
|
+
(tools entry not set up), or is missing entirely, the entry list is ``[]``
|
|
2198
|
+
and ``unavailable_reason`` carries the human-readable cause — a
|
|
2199
|
+
``HomeAssistantError`` or any ``ToolError`` out of
|
|
2200
|
+
``call_mcp_tools_service`` (all its caller-token gates included) lands
|
|
2201
|
+
here (also logged at debug), so the edits ``list`` still works and the
|
|
2202
|
+
caller can surface the degradation as a warning. A ``success: False``
|
|
2203
|
+
service response degrades silently as before. Genuine programming errors
|
|
2204
|
+
propagate.
|
|
2184
2205
|
"""
|
|
2206
|
+
from .tools.helpers import extract_structured_error_reason
|
|
2185
2207
|
from .tools.tools_filesystem import call_mcp_tools_service
|
|
2186
2208
|
from .tools.util_helpers import unwrap_service_response
|
|
2187
2209
|
|
|
2188
2210
|
try:
|
|
2189
2211
|
result = await call_mcp_tools_service(client, "list_legacy_backups", {})
|
|
2190
|
-
except HomeAssistantError as err:
|
|
2212
|
+
except (HomeAssistantError, ToolError) as err:
|
|
2191
2213
|
logger.debug("legacy backup list unavailable: %s", err)
|
|
2192
|
-
return []
|
|
2214
|
+
return [], extract_structured_error_reason(err) or str(err)
|
|
2193
2215
|
if not isinstance(result, dict):
|
|
2194
|
-
return []
|
|
2216
|
+
return [], None
|
|
2195
2217
|
result = unwrap_service_response(result)
|
|
2196
2218
|
if not result.get("success", False):
|
|
2197
|
-
return []
|
|
2219
|
+
return [], None
|
|
2198
2220
|
backups = result.get("backups")
|
|
2199
|
-
return backups if isinstance(backups, list) else []
|
|
2221
|
+
return (backups, None) if isinstance(backups, list) else ([], None)
|
|
2200
2222
|
|
|
2201
2223
|
|
|
2202
2224
|
async def _read_legacy_backup(client: Any, filename: str) -> dict[str, Any]:
|
|
@@ -2204,9 +2226,11 @@ async def _read_legacy_backup(client: Any, filename: str) -> dict[str, Any]:
|
|
|
2204
2226
|
|
|
2205
2227
|
Returns the unwrapped service response (carries ``success`` / ``content`` /
|
|
2206
2228
|
``file_path`` / ``path_ambiguous`` / ``timestamp``). A service-unavailable
|
|
2207
|
-
``HomeAssistantError``
|
|
2208
|
-
surfaces a not-found
|
|
2229
|
+
``HomeAssistantError`` — or a ``ToolError`` from the caller-token gates —
|
|
2230
|
+
is mapped to a ``success: False`` dict so the caller surfaces a not-found
|
|
2231
|
+
rather than crashing.
|
|
2209
2232
|
"""
|
|
2233
|
+
from .tools.helpers import extract_structured_error_reason
|
|
2210
2234
|
from .tools.tools_filesystem import call_mcp_tools_service
|
|
2211
2235
|
from .tools.util_helpers import unwrap_service_response
|
|
2212
2236
|
|
|
@@ -2214,8 +2238,15 @@ async def _read_legacy_backup(client: Any, filename: str) -> dict[str, Any]:
|
|
|
2214
2238
|
result = await call_mcp_tools_service(
|
|
2215
2239
|
client, "read_legacy_backup", {"filename": filename}
|
|
2216
2240
|
)
|
|
2217
|
-
except HomeAssistantError as err:
|
|
2218
|
-
|
|
2241
|
+
except (HomeAssistantError, ToolError) as err:
|
|
2242
|
+
# The human reason, not str(err) verbatim: a ToolError's string is
|
|
2243
|
+
# the whole JSON envelope, which would reach the tool layer as an
|
|
2244
|
+
# unreadable JSON-in-JSON message (#1996's original symptom).
|
|
2245
|
+
logger.debug("legacy backup read unavailable: %s", err)
|
|
2246
|
+
return {
|
|
2247
|
+
"success": False,
|
|
2248
|
+
"error": extract_structured_error_reason(err) or str(err),
|
|
2249
|
+
}
|
|
2219
2250
|
if not isinstance(result, dict):
|
|
2220
2251
|
return {"success": False, "error": f"no response for {filename!r}"}
|
|
2221
2252
|
return unwrap_service_response(result)
|
{ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/settings_ui/_handlers_fs.py
RENAMED
|
@@ -25,6 +25,20 @@ if TYPE_CHECKING:
|
|
|
25
25
|
logger = logging.getLogger(__name__)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _component_error_reason(exc: Exception) -> str | None:
|
|
29
|
+
"""Extract the human message from a structured ``ToolError``, else None.
|
|
30
|
+
|
|
31
|
+
``raise_tool_error`` serializes the whole error envelope into the
|
|
32
|
+
exception string, so rendering ``str(exc)`` verbatim shows the settings
|
|
33
|
+
UI a raw JSON blob behind a misleading "could not reach" prefix — the
|
|
34
|
+
hard-to-read surface from #1996. Thin seam over the shared extractor
|
|
35
|
+
(imported lazily like the other tools imports in this module).
|
|
36
|
+
"""
|
|
37
|
+
from ..tools.helpers import extract_structured_error_reason
|
|
38
|
+
|
|
39
|
+
return extract_structured_error_reason(exc)
|
|
40
|
+
|
|
41
|
+
|
|
28
42
|
async def _fs_custom_paths_call(
|
|
29
43
|
server: HomeAssistantSmartMCPServer | None, service: str, data: dict[str, Any]
|
|
30
44
|
) -> Any:
|
|
@@ -90,7 +104,10 @@ async def _get_fs_custom_paths(
|
|
|
90
104
|
result = await _fs_custom_paths_call(server, "get_allowed_paths", {})
|
|
91
105
|
except Exception as exc:
|
|
92
106
|
logger.warning("fs-custom-paths GET could not reach ha_mcp_tools: %s", exc)
|
|
93
|
-
return _unavailable(
|
|
107
|
+
return _unavailable(
|
|
108
|
+
_component_error_reason(exc)
|
|
109
|
+
or f"Could not reach the ha_mcp_tools component: {exc}"
|
|
110
|
+
)
|
|
94
111
|
|
|
95
112
|
data = unwrap_service_response(result) if isinstance(result, dict) else {}
|
|
96
113
|
if not isinstance(data, dict) or not data.get("success", False):
|
|
@@ -163,7 +180,8 @@ async def _save_fs_custom_paths(
|
|
|
163
180
|
return JSONResponse(
|
|
164
181
|
create_error_response(
|
|
165
182
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
166
|
-
|
|
183
|
+
_component_error_reason(exc)
|
|
184
|
+
or f"Could not reach the ha_mcp_tools component: {exc}",
|
|
167
185
|
),
|
|
168
186
|
status_code=502,
|
|
169
187
|
)
|
|
@@ -1908,12 +1908,12 @@ async def _edits_list(
|
|
|
1908
1908
|
# Edits-store snapshots + pre-#1579 legacy .bak entries (#1579).
|
|
1909
1909
|
# The manager owns the merge (sync dir-glob off-thread + async
|
|
1910
1910
|
# legacy service call) so this layer stays source-agnostic.
|
|
1911
|
-
entries = await mgr.list_edits_and_legacy(
|
|
1911
|
+
entries, warnings = await mgr.list_edits_and_legacy(
|
|
1912
1912
|
domain=domain,
|
|
1913
1913
|
entity_id=entity_id,
|
|
1914
1914
|
limit=limit,
|
|
1915
1915
|
)
|
|
1916
|
-
|
|
1916
|
+
response: dict[str, Any] = {
|
|
1917
1917
|
"success": True,
|
|
1918
1918
|
"data": {
|
|
1919
1919
|
"backups": entries,
|
|
@@ -1924,6 +1924,9 @@ async def _edits_list(
|
|
|
1924
1924
|
"retain_per_entity": settings.auto_backup_retain_per_entity,
|
|
1925
1925
|
},
|
|
1926
1926
|
}
|
|
1927
|
+
if warnings:
|
|
1928
|
+
response["warnings"] = warnings
|
|
1929
|
+
return response
|
|
1927
1930
|
|
|
1928
1931
|
|
|
1929
1932
|
async def _edits_view(
|
|
@@ -78,6 +78,41 @@ def extract_tool_error_message(te: ToolError) -> str:
|
|
|
78
78
|
return str(te)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def extract_structured_error_reason(exc: BaseException) -> str | None:
|
|
82
|
+
"""Extract "message" + first actionable suggestion from a structured error.
|
|
83
|
+
|
|
84
|
+
Like ``extract_tool_error_message`` but for surfaces that show the reason
|
|
85
|
+
to a human (settings UI, degraded-result warnings): it appends the first
|
|
86
|
+
suggestion when one exists, and returns ``None`` — instead of the raw
|
|
87
|
+
``str(exc)`` JSON envelope — when the payload is not a structured error
|
|
88
|
+
or carries no usable message, so callers pick their own fallback.
|
|
89
|
+
``create_error_response`` emits only the singular ``suggestion`` key for
|
|
90
|
+
one-suggestion errors (the plural ``suggestions`` list needs two or
|
|
91
|
+
more), so both keys are honored.
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
payload = json.loads(str(exc))
|
|
95
|
+
except (TypeError, ValueError):
|
|
96
|
+
return None
|
|
97
|
+
if not isinstance(payload, dict):
|
|
98
|
+
return None
|
|
99
|
+
error = payload.get("error")
|
|
100
|
+
if not isinstance(error, dict):
|
|
101
|
+
return None
|
|
102
|
+
message = error.get("message")
|
|
103
|
+
if not isinstance(message, str) or not message:
|
|
104
|
+
return None
|
|
105
|
+
suggestions = error.get("suggestions")
|
|
106
|
+
first = (
|
|
107
|
+
suggestions[0]
|
|
108
|
+
if isinstance(suggestions, list) and suggestions
|
|
109
|
+
else error.get("suggestion")
|
|
110
|
+
)
|
|
111
|
+
if isinstance(first, str) and first:
|
|
112
|
+
return f"{message} {first}"
|
|
113
|
+
return message
|
|
114
|
+
|
|
115
|
+
|
|
81
116
|
def validate_identifier_not_empty(
|
|
82
117
|
value: str | None,
|
|
83
118
|
param_name: str,
|
{ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_bug_report.py
RENAMED
|
@@ -556,6 +556,49 @@ async def _fetch_core_error_log(client: Any) -> str:
|
|
|
556
556
|
return sanitized
|
|
557
557
|
|
|
558
558
|
|
|
559
|
+
def _format_tools_entry_value(diagnostic_info: dict[str, Any]) -> str:
|
|
560
|
+
"""Render the File & YAML Tools entry status for the report templates."""
|
|
561
|
+
return diagnostic_info.get("tools_entry_status") or "unknown (probe failed)"
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _format_server_entry_value(diagnostic_info: dict[str, Any]) -> str:
|
|
565
|
+
"""Render the in-process server entry status for the report templates."""
|
|
566
|
+
return diagnostic_info.get("server_entry_status") or "unknown (probe failed)"
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# Entry titles assigned by the component's config flow (config_flow.py /
|
|
570
|
+
# const.py in custom_components/ha_mcp_tools) — the server code cannot import
|
|
571
|
+
# the separately-shipped component package, so the defaults are mirrored here.
|
|
572
|
+
# "HA MCP Tools" is the pre-#1853 tools-entry title an unloaded legacy entry
|
|
573
|
+
# may still carry. A user-renamed entry lands in the "unrecognized" bucket
|
|
574
|
+
# rather than being misclassified.
|
|
575
|
+
_SERVER_ENTRY_TITLE = "HA-MCP Server"
|
|
576
|
+
_TOOLS_ENTRY_TITLES = frozenset({"HA-MCP File & YAML Tools", "HA MCP Tools"})
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _classify_component_entries(
|
|
580
|
+
entries: list[Any],
|
|
581
|
+
) -> tuple[list[str], list[str]]:
|
|
582
|
+
"""Split ha_mcp_tools config entries into server-entry and unrecognized.
|
|
583
|
+
|
|
584
|
+
Tools-entry titles are dropped: their functional state comes from the
|
|
585
|
+
services probe (``_detect_tools_entry_status``), which sees whether the
|
|
586
|
+
entry actually serves, not just whether it exists.
|
|
587
|
+
"""
|
|
588
|
+
server: list[str] = []
|
|
589
|
+
unrecognized: list[str] = []
|
|
590
|
+
for entry in entries:
|
|
591
|
+
if not isinstance(entry, dict):
|
|
592
|
+
continue
|
|
593
|
+
title = str(entry.get("title") or "untitled")
|
|
594
|
+
state = str(entry.get("state") or "unknown state")
|
|
595
|
+
if title == _SERVER_ENTRY_TITLE:
|
|
596
|
+
server.append(f"added ({state})")
|
|
597
|
+
elif title not in _TOOLS_ENTRY_TITLES:
|
|
598
|
+
unrecognized.append(f'"{title}" ({state})')
|
|
599
|
+
return server, unrecognized
|
|
600
|
+
|
|
601
|
+
|
|
559
602
|
def _build_formatted_report(
|
|
560
603
|
diagnostic_info: dict[str, Any],
|
|
561
604
|
mcp_transport: str,
|
|
@@ -574,6 +617,8 @@ def _build_formatted_report(
|
|
|
574
617
|
"",
|
|
575
618
|
f"ha-mcp Version: {_format_version_value(diagnostic_info)}",
|
|
576
619
|
f"Custom Component: {diagnostic_info.get('component_version') or 'not detected (not installed, or probe failed)'}",
|
|
620
|
+
f"File & YAML Tools entry: {_format_tools_entry_value(diagnostic_info)}",
|
|
621
|
+
f"In-process Server entry: {_format_server_entry_value(diagnostic_info)}",
|
|
577
622
|
f"Installation Method: {diagnostic_info['installation_method']}",
|
|
578
623
|
f"MCP Transport: {mcp_transport}",
|
|
579
624
|
f"MCP Client: {_format_client_info_for_template(client_info)}",
|
|
@@ -649,6 +694,70 @@ class BugReportTools:
|
|
|
649
694
|
logger.info("Component version probe failed: %s", e)
|
|
650
695
|
return None
|
|
651
696
|
|
|
697
|
+
async def _detect_tools_entry_status(self) -> str | None:
|
|
698
|
+
"""Describe the File & YAML Tools entry state for the report, best-effort.
|
|
699
|
+
|
|
700
|
+
The ``ha_mcp_tools`` services register only in that entry's setup, so
|
|
701
|
+
the service registry distinguishes "entry set up" from the #1996
|
|
702
|
+
state — integration installed but the second entry never added —
|
|
703
|
+
which the component version alone cannot show. Returns None when the
|
|
704
|
+
probe fails; the report path must never break on it.
|
|
705
|
+
"""
|
|
706
|
+
from .tools_filesystem import _bootstrap_service_state
|
|
707
|
+
|
|
708
|
+
try:
|
|
709
|
+
domain_registered, bootstrap_registered = await _bootstrap_service_state(
|
|
710
|
+
self._client
|
|
711
|
+
)
|
|
712
|
+
except Exception as e:
|
|
713
|
+
logger.info("Tools-entry status probe failed: %s", e)
|
|
714
|
+
return None
|
|
715
|
+
if not domain_registered:
|
|
716
|
+
return (
|
|
717
|
+
"not set up — no ha_mcp_tools services registered; add the "
|
|
718
|
+
'"HA-MCP File & YAML Tools" entry via "Add entry" on the '
|
|
719
|
+
"HA-MCP Custom Component integration (or install the "
|
|
720
|
+
"component first)"
|
|
721
|
+
)
|
|
722
|
+
if not bootstrap_registered:
|
|
723
|
+
return "set up, but the component is pre-0.5.0 (update via HACS)"
|
|
724
|
+
return "set up (ha_mcp_tools services registered)"
|
|
725
|
+
|
|
726
|
+
async def _detect_server_entry_status(self) -> str | None:
|
|
727
|
+
"""Describe the in-process "HA-MCP Server" entry state, best-effort.
|
|
728
|
+
|
|
729
|
+
Both parts of the custom component ship under the single
|
|
730
|
+
``ha_mcp_tools`` domain, so "the component is installed" says nothing
|
|
731
|
+
about WHICH part is set up. The File & YAML tools part is covered by
|
|
732
|
+
the services probe; this one classifies the domain's config entries by
|
|
733
|
+
their flow-assigned titles to show whether the in-process server entry
|
|
734
|
+
exists — e.g. an add-on install that erroneously also added it (a
|
|
735
|
+
second, redundant server) becomes visible next to Installation Method.
|
|
736
|
+
Returns None when the probe fails; the report must never break on it.
|
|
737
|
+
"""
|
|
738
|
+
try:
|
|
739
|
+
response = await self._client.send_websocket_message(
|
|
740
|
+
{"type": "config_entries/get", "domain": "ha_mcp_tools"}
|
|
741
|
+
)
|
|
742
|
+
if not isinstance(response, dict) or not response.get("success"):
|
|
743
|
+
return None
|
|
744
|
+
result = response.get("result")
|
|
745
|
+
if not isinstance(result, list):
|
|
746
|
+
return None
|
|
747
|
+
except Exception as e:
|
|
748
|
+
logger.info("Server-entry status probe failed: %s", e)
|
|
749
|
+
return None
|
|
750
|
+
server, unrecognized = _classify_component_entries(result)
|
|
751
|
+
if server:
|
|
752
|
+
return ", ".join(server)
|
|
753
|
+
if unrecognized:
|
|
754
|
+
# A renamed entry cannot be classified by title — report it
|
|
755
|
+
# rather than claiming the server entry is absent.
|
|
756
|
+
return "not identified — unrecognized ha_mcp_tools entries: " + ", ".join(
|
|
757
|
+
unrecognized
|
|
758
|
+
)
|
|
759
|
+
return "not added"
|
|
760
|
+
|
|
652
761
|
@tool(
|
|
653
762
|
name="ha_report_issue",
|
|
654
763
|
tags={"Utilities"},
|
|
@@ -760,6 +869,8 @@ class BugReportTools:
|
|
|
760
869
|
client_info = _extract_client_info(ctx)
|
|
761
870
|
installed_version = await asyncio.to_thread(_detect_installed_version)
|
|
762
871
|
component_version = await self._detect_component_version()
|
|
872
|
+
tools_entry_status = await self._detect_tools_entry_status()
|
|
873
|
+
server_entry_status = await self._detect_server_entry_status()
|
|
763
874
|
|
|
764
875
|
diagnostic_info: dict[str, Any] = {
|
|
765
876
|
"ha_mcp_version": __version__,
|
|
@@ -768,6 +879,8 @@ class BugReportTools:
|
|
|
768
879
|
installed_version and installed_version != __version__
|
|
769
880
|
),
|
|
770
881
|
"component_version": component_version,
|
|
882
|
+
"tools_entry_status": tools_entry_status,
|
|
883
|
+
"server_entry_status": server_entry_status,
|
|
771
884
|
"instance": _instance_identity(),
|
|
772
885
|
"installation_method": install_method,
|
|
773
886
|
"platform": platform_info,
|
|
@@ -1278,6 +1391,8 @@ ha_call_service(domain="light", service="turn_on", entity_id="light.example")
|
|
|
1278
1391
|
|
|
1279
1392
|
- **ha-mcp Version:** {_format_version_value(diagnostic_info)}
|
|
1280
1393
|
- **Custom Component:** {diagnostic_info.get("component_version") or "not detected (not installed, or probe failed)"}
|
|
1394
|
+
- **File & YAML Tools entry:** {_format_tools_entry_value(diagnostic_info)}
|
|
1395
|
+
- **In-process Server entry:** {_format_server_entry_value(diagnostic_info)}
|
|
1281
1396
|
- **Installation Method:** {diagnostic_info.get("installation_method", "Unknown")}
|
|
1282
1397
|
- **MCP Transport:** {mcp_transport} _(auto-detected — correct if wrong)_
|
|
1283
1398
|
- **MCP Client:** {_format_client_info_for_template(client_info)} _(auto-detected from the MCP `initialize` handshake)_
|
|
@@ -1443,6 +1558,8 @@ def _generate_agent_behavior_template(
|
|
|
1443
1558
|
|
|
1444
1559
|
- **ha-mcp Version:** {_format_version_value(diagnostic_info)}
|
|
1445
1560
|
- **Custom Component:** {diagnostic_info.get("component_version") or "not detected (not installed, or probe failed)"}
|
|
1561
|
+
- **File & YAML Tools entry:** {_format_tools_entry_value(diagnostic_info)}
|
|
1562
|
+
- **In-process Server entry:** {_format_server_entry_value(diagnostic_info)}
|
|
1446
1563
|
- **Installation Method:** {diagnostic_info.get("installation_method", "Unknown")}
|
|
1447
1564
|
- **MCP Transport:** {mcp_transport} _(auto-detected — correct if wrong)_
|
|
1448
1565
|
- **MCP Client:** {_format_client_info_for_template(client_info)} _(auto-detected from the MCP `initialize` handshake)_
|
{ha_mcp_dev-7.14.2.dev2095 → ha_mcp_dev-7.14.2.dev2098}/src/ha_mcp/tools/tools_filesystem.py
RENAMED
|
@@ -122,11 +122,23 @@ def _get_token_lock(client: Any) -> asyncio.Lock:
|
|
|
122
122
|
return lock
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
async def
|
|
126
|
-
"""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
async def _bootstrap_service_state(client: Any) -> tuple[bool, bool]:
|
|
126
|
+
"""Return ``(domain_registered, bootstrap_registered)`` from HA's service
|
|
127
|
+
registry.
|
|
128
|
+
|
|
129
|
+
The two flags distinguish the two very different ways the
|
|
130
|
+
``get_caller_token`` bootstrap can be missing (#1996):
|
|
131
|
+
|
|
132
|
+
- No ``ha_mcp_tools`` services at all — the component's "HA-MCP File &
|
|
133
|
+
YAML Tools" config entry was never added (the services register only in
|
|
134
|
+
that entry's setup), or the component isn't installed. A HACS update
|
|
135
|
+
cannot fix this.
|
|
136
|
+
- Domain services present but no ``get_caller_token`` — a genuinely old
|
|
137
|
+
(<0.5.0) component that pre-dates the bootstrap service; the call would
|
|
138
|
+
otherwise fail with an opaque 400 from HA. This one IS fixed by updating.
|
|
139
|
+
"""
|
|
140
|
+
# HA /api/services returns a list of {"domain": str, "services": {...}}
|
|
141
|
+
# objects — stable across all supported HA versions.
|
|
130
142
|
services = await client.get_services()
|
|
131
143
|
for entry in services:
|
|
132
144
|
if not isinstance(entry, dict):
|
|
@@ -134,8 +146,42 @@ async def _is_bootstrap_service_registered(client: Any) -> bool:
|
|
|
134
146
|
if entry.get("domain") != MCP_TOOLS_DOMAIN:
|
|
135
147
|
continue
|
|
136
148
|
domain_services = entry.get("services") or {}
|
|
137
|
-
return CALLER_TOKEN_BOOTSTRAP_SERVICE in domain_services
|
|
138
|
-
return False
|
|
149
|
+
return True, CALLER_TOKEN_BOOTSTRAP_SERVICE in domain_services
|
|
150
|
+
return False, False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _raise_tools_entry_not_set_up() -> NoReturn:
|
|
154
|
+
"""Actionable error for the no-services state (#1996).
|
|
155
|
+
|
|
156
|
+
Installing the HA-MCP integration is not enough for the file / YAML
|
|
157
|
+
tools: they need the separate "HA-MCP File & YAML Tools" config entry,
|
|
158
|
+
which users routinely miss. Lead with the "Add entry" step; the full
|
|
159
|
+
HACS install is the fallback for a component that isn't there at all.
|
|
160
|
+
"""
|
|
161
|
+
raise_tool_error(
|
|
162
|
+
create_error_response(
|
|
163
|
+
ErrorCode.COMPONENT_NOT_INSTALLED,
|
|
164
|
+
'The optional "HA-MCP File & YAML Tools" entry is not set up in '
|
|
165
|
+
"Home Assistant, so the file and YAML tools are unavailable. "
|
|
166
|
+
"Installing the HA-MCP integration alone is not enough — this "
|
|
167
|
+
"second entry must be added to it.",
|
|
168
|
+
suggestions=[
|
|
169
|
+
"In Home Assistant: Settings → Devices & Services → "
|
|
170
|
+
+ 'HA-MCP Custom Component → "Add entry" → choose '
|
|
171
|
+
+ '"HA-MCP File & YAML Tools" (NOT '
|
|
172
|
+
+ '"HA-MCP Server", which starts a second in-process server '
|
|
173
|
+
+ "this ha-mcp server does not need)",
|
|
174
|
+
"If the HA-MCP integration is not listed there, install it "
|
|
175
|
+
+ 'first: ha_manage_hacs(action="add_repository", '
|
|
176
|
+
+ 'repository="homeassistant-ai/ha-mcp-integration", '
|
|
177
|
+
+ 'category="integration"), then ha_manage_hacs('
|
|
178
|
+
+ 'action="download", '
|
|
179
|
+
+ 'repository_id="homeassistant-ai/ha-mcp-integration"), '
|
|
180
|
+
+ "then restart Home Assistant (ha_restart)",
|
|
181
|
+
"Then retry the operation",
|
|
182
|
+
],
|
|
183
|
+
)
|
|
184
|
+
)
|
|
139
185
|
|
|
140
186
|
|
|
141
187
|
def _raise_component_too_old(detail: str) -> NoReturn:
|
|
@@ -158,12 +204,17 @@ def _raise_component_too_old(detail: str) -> NoReturn:
|
|
|
158
204
|
async def _fetch_caller_token(client: Any) -> str:
|
|
159
205
|
"""Call the bootstrap service and cache the returned token.
|
|
160
206
|
|
|
161
|
-
|
|
207
|
+
Three gates, pre-flighted via ``_bootstrap_service_state``:
|
|
162
208
|
|
|
163
|
-
1. ``
|
|
164
|
-
|
|
209
|
+
1. No ``ha_mcp_tools`` services registered at all — the "HA-MCP File &
|
|
210
|
+
YAML Tools" config entry was never added (or the component isn't
|
|
211
|
+
installed). Surface the "add the entry" error, NOT an update prompt:
|
|
212
|
+
a current component in this state misled users into fruitless HACS
|
|
213
|
+
updates when the old code lumped it in with "too old" (#1996).
|
|
214
|
+
2. Domain present but no ``get_caller_token`` — pre-0.5.0 components
|
|
215
|
+
don't ship the bootstrap service. Surface an actionable "update"
|
|
165
216
|
error instead of letting HA return an opaque 400 to the caller.
|
|
166
|
-
|
|
217
|
+
3. ``MIN_COMPONENT_VERSION`` — even when the bootstrap service is
|
|
167
218
|
present, the response now carries the component's manifest
|
|
168
219
|
version. ha-mcp releases that depend on newer custom-component
|
|
169
220
|
behavior (e.g. a new accepted yaml_path key, a new schema field)
|
|
@@ -176,7 +227,10 @@ async def _fetch_caller_token(client: Any) -> str:
|
|
|
176
227
|
reason: the absence of the field IS the signal that the component
|
|
177
228
|
doesn't yet know how to report its capabilities to ha-mcp.
|
|
178
229
|
"""
|
|
179
|
-
|
|
230
|
+
domain_registered, bootstrap_registered = await _bootstrap_service_state(client)
|
|
231
|
+
if not domain_registered:
|
|
232
|
+
_raise_tools_entry_not_set_up()
|
|
233
|
+
if not bootstrap_registered:
|
|
180
234
|
_raise_component_too_old(
|
|
181
235
|
"the get_caller_token bootstrap service is not registered (pre-0.5.0)"
|
|
182
236
|
)
|
|
@@ -312,17 +366,16 @@ def is_filesystem_tools_enabled() -> bool:
|
|
|
312
366
|
|
|
313
367
|
|
|
314
368
|
async def _is_mcp_tools_available(client: Any) -> bool:
|
|
315
|
-
"""Return True if
|
|
369
|
+
"""Return True if any ha_mcp_tools services are registered in HA.
|
|
316
370
|
|
|
317
|
-
|
|
318
|
-
|
|
371
|
+
No services means the File & YAML Tools entry is not set up (or the
|
|
372
|
+
component is absent entirely — indistinguishable from here, see
|
|
373
|
+
``_bootstrap_service_state``). Raises if the services API call fails —
|
|
374
|
+
callers handle API errors via their own exception_to_structured_error
|
|
375
|
+
blocks.
|
|
319
376
|
"""
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
services = await client.get_services()
|
|
323
|
-
return any(
|
|
324
|
-
isinstance(s, dict) and s.get("domain") == MCP_TOOLS_DOMAIN for s in services
|
|
325
|
-
)
|
|
377
|
+
domain_registered, _ = await _bootstrap_service_state(client)
|
|
378
|
+
return domain_registered
|
|
326
379
|
|
|
327
380
|
|
|
328
381
|
def _assert_caps_version_ok(caps: ComponentCaps) -> None:
|
|
@@ -352,7 +405,10 @@ async def _assert_mcp_tools_available(client: Any) -> None:
|
|
|
352
405
|
``caps.component_version`` (info shipped in 1.1.0, already past the floor).
|
|
353
406
|
Legacy fallback: caps is None for a component in the 0.11.0-1.1.0 band
|
|
354
407
|
(services, no info command) or an absent one, so fall back to the per-call
|
|
355
|
-
``get_services()`` existence probe.
|
|
408
|
+
``get_services()`` existence probe. A no-services verdict raises the
|
|
409
|
+
"File & YAML Tools entry not set up" error
|
|
410
|
+
(``_raise_tools_entry_not_set_up``), not an install-the-component prompt —
|
|
411
|
+
the entry-not-added state is the common cause (#1996).
|
|
356
412
|
|
|
357
413
|
Must be called within a try block that handles API errors via
|
|
358
414
|
exception_to_structured_error, so connection failures are classified
|
|
@@ -365,22 +421,7 @@ async def _assert_mcp_tools_available(client: Any) -> None:
|
|
|
365
421
|
_assert_caps_version_ok(caps)
|
|
366
422
|
return
|
|
367
423
|
if not await _is_mcp_tools_available(client):
|
|
368
|
-
|
|
369
|
-
create_error_response(
|
|
370
|
-
ErrorCode.COMPONENT_NOT_INSTALLED,
|
|
371
|
-
f"The {MCP_TOOLS_DOMAIN} custom component is not installed.",
|
|
372
|
-
suggestions=[
|
|
373
|
-
'Add the repository to HACS: ha_manage_hacs(action="add_repository",'
|
|
374
|
-
+ ' repository="homeassistant-ai/ha-mcp-integration", category="integration")',
|
|
375
|
-
'Download the component: ha_manage_hacs(action="download",'
|
|
376
|
-
+ ' repository_id="homeassistant-ai/ha-mcp-integration")',
|
|
377
|
-
"Restart Home Assistant (ha_restart) so the integration loads",
|
|
378
|
-
'In HA, add the "HA-MCP Custom Component" integration and choose the'
|
|
379
|
-
+ ' "HA-MCP File & YAML Tools" entry — NOT "HA-MCP Server", which starts'
|
|
380
|
-
+ " a second in-process server this ha-mcp server does not need",
|
|
381
|
-
],
|
|
382
|
-
)
|
|
383
|
-
)
|
|
424
|
+
_raise_tools_entry_not_set_up()
|
|
384
425
|
|
|
385
426
|
|
|
386
427
|
class FilesystemTools:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|