ha-mcp-dev 7.12.3.dev1923__tar.gz → 7.12.3.dev1926__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.12.3.dev1923/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1926}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/pyproject.toml +1 -2
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/client/supervisor_client.py +2 -2
- ha_mcp_dev-7.12.3.dev1926/src/ha_mcp/settings_ui/__init__.py +599 -0
- ha_mcp_dev-7.12.3.dev1926/src/ha_mcp/settings_ui/_handlers_advanced.py +599 -0
- ha_mcp_dev-7.12.3.dev1926/src/ha_mcp/settings_ui/_handlers_backups.py +523 -0
- ha_mcp_dev-7.12.3.dev1926/src/ha_mcp/settings_ui/_handlers_fs.py +204 -0
- ha_mcp_dev-7.12.3.dev1926/src/ha_mcp/settings_ui/_handlers_server.py +774 -0
- ha_mcp_dev-7.12.3.dev1926/src/ha_mcp/settings_ui/_handlers_theme.py +89 -0
- ha_mcp_dev-7.12.3.dev1926/src/ha_mcp/settings_ui/_handlers_tools.py +299 -0
- ha_mcp_dev-7.12.3.dev1926/src/ha_mcp/settings_ui/_persistence.py +333 -0
- ha_mcp_dev-7.12.3.dev1926/src/ha_mcp/settings_ui/_supervisor.py +294 -0
- ha_mcp_dev-7.12.3.dev1926/src/ha_mcp/settings_ui/_theme.py +133 -0
- ha_mcp_dev-7.12.3.dev1926/src/ha_mcp/settings_ui/_tools_meta.py +332 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_dev.py +8 -5
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp_dev.egg-info/SOURCES.txt +10 -0
- ha_mcp_dev-7.12.3.dev1923/src/ha_mcp/settings_ui/__init__.py +0 -3716
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/LICENSE +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/README.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/setup.cfg +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/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.12.3.
|
|
7
|
+
version = "7.12.3.dev1926"
|
|
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
|
|
@@ -175,7 +175,6 @@ ignore = [
|
|
|
175
175
|
"homeassistant-addon-webhook-proxy/start.py" = ["C901"]
|
|
176
176
|
"scripts/extract_tools.py" = ["C901"]
|
|
177
177
|
"src/ha_mcp/backup_manager.py" = ["C901"]
|
|
178
|
-
"src/ha_mcp/settings_ui/__init__.py" = ["C901"]
|
|
179
178
|
"src/ha_mcp/tools/auto_backup.py" = ["C901"]
|
|
180
179
|
"src/ha_mcp/tools/backup.py" = ["C901"]
|
|
181
180
|
"src/ha_mcp/tools/radio/matter.py" = ["C901"]
|
{ha_mcp_dev-7.12.3.dev1923 → ha_mcp_dev-7.12.3.dev1926}/src/ha_mcp/client/supervisor_client.py
RENAMED
|
@@ -9,8 +9,8 @@ Supervisor — different base URL, different token, different role gate):
|
|
|
9
9
|
— fetches addon and system-service logs
|
|
10
10
|
- :func:`ha_mcp.tools.tools_bug_report._fetch_addon_logs` — bundles ha-mcp's
|
|
11
11
|
own addon logs into a bug-report payload
|
|
12
|
-
- :func:`ha_mcp.settings_ui._restart_addon` — POSTs
|
|
13
|
-
from the settings UI
|
|
12
|
+
- :func:`ha_mcp.settings_ui._handlers_server._restart_addon` — POSTs
|
|
13
|
+
``/addons/self/restart`` from the settings UI
|
|
14
14
|
|
|
15
15
|
All three share the same boilerplate (base URL, ``Authorization: Bearer
|
|
16
16
|
${SUPERVISOR_TOKEN}`` header), so this module supplies a single factory and
|
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
"""Web-based settings UI for tool visibility configuration.
|
|
2
|
+
|
|
3
|
+
Serves a self-contained HTML page at /settings that lets users enable,
|
|
4
|
+
disable, and pin MCP tools. Changes apply immediately without server
|
|
5
|
+
restart. Persists to a JSON config file alongside the MCP server data.
|
|
6
|
+
|
|
7
|
+
Works across all installation methods (add-on, Docker, standalone).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import functools
|
|
13
|
+
import html
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
from collections.abc import Awaitable, Callable
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
from starlette.requests import Request
|
|
21
|
+
from starlette.responses import HTMLResponse, JSONResponse, Response
|
|
22
|
+
|
|
23
|
+
from .._version import is_running_in_addon
|
|
24
|
+
from ..transforms import DEFAULT_PINNED_TOOLS
|
|
25
|
+
from ..utils.data_paths import get_data_dir
|
|
26
|
+
from ._handlers_advanced import build_advanced_handlers
|
|
27
|
+
from ._handlers_backups import build_backups_handlers
|
|
28
|
+
from ._handlers_fs import build_fs_handlers
|
|
29
|
+
from ._handlers_server import (
|
|
30
|
+
_PROCESS_INSTANCE_ID,
|
|
31
|
+
_PROCESS_STARTED_AT,
|
|
32
|
+
build_server_handlers,
|
|
33
|
+
)
|
|
34
|
+
from ._handlers_theme import build_theme_handlers
|
|
35
|
+
from ._handlers_tools import build_tools_handlers
|
|
36
|
+
from ._persistence import (
|
|
37
|
+
_atomic_write_json,
|
|
38
|
+
_get_backup_settings_override_path,
|
|
39
|
+
_get_config_path,
|
|
40
|
+
_get_override_file_lock,
|
|
41
|
+
_load_backup_settings_override,
|
|
42
|
+
_save_backup_settings_override,
|
|
43
|
+
dump_tool_metadata_cache,
|
|
44
|
+
effective_tool_config,
|
|
45
|
+
env_pinned_tools,
|
|
46
|
+
load_tool_config,
|
|
47
|
+
load_tool_metadata_cache,
|
|
48
|
+
save_tool_config,
|
|
49
|
+
)
|
|
50
|
+
from ._supervisor import (
|
|
51
|
+
_BACKGROUND_RESTART_TASKS,
|
|
52
|
+
_SUPERVISOR_SELF_RESTART_FLUSH_DELAY_S,
|
|
53
|
+
_schedule_supervisor_self_restart,
|
|
54
|
+
_supervisor_fetch_current_options,
|
|
55
|
+
_supervisor_merge_and_post_options,
|
|
56
|
+
_SupervisorOptionsError,
|
|
57
|
+
)
|
|
58
|
+
from ._theme import _load_theme_prefs, _sanitize_theme_prefs
|
|
59
|
+
from ._tools_meta import (
|
|
60
|
+
FEATURE_GATED_TOOLS,
|
|
61
|
+
MANDATORY_TOOLS,
|
|
62
|
+
TRANSFORM_GENERATED_TOOLS,
|
|
63
|
+
ToolStub,
|
|
64
|
+
UserToolStateOverrides,
|
|
65
|
+
_get_tool_metadata,
|
|
66
|
+
apply_tool_visibility,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if TYPE_CHECKING:
|
|
70
|
+
from fastmcp import FastMCP
|
|
71
|
+
|
|
72
|
+
from ..config import Settings
|
|
73
|
+
from ..server import HomeAssistantSmartMCPServer
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
logger = logging.getLogger(__name__)
|
|
77
|
+
|
|
78
|
+
# The settings-UI client script lives in settings.js (a real file for
|
|
79
|
+
# editor/JS tooling). It is a template with two sentinel tokens for the
|
|
80
|
+
# Python-injected constant lists; substitute them with the same values the
|
|
81
|
+
# inline literal used so the rendered HTML is byte-identical. Injected
|
|
82
|
+
# inline (not served) -- the serving model is unchanged.
|
|
83
|
+
#
|
|
84
|
+
# This is a module-import-time file read: importing settings_ui (done by
|
|
85
|
+
# server.py / __main__.py / the sidecar) now depends on settings.js being
|
|
86
|
+
# present. If packaging drops it, fail with a packaging-specific ImportError
|
|
87
|
+
# rather than a bare FileNotFoundError so the cause is obvious.
|
|
88
|
+
_SETTINGS_JS_PATH = Path(__file__).parent / "settings.js"
|
|
89
|
+
try:
|
|
90
|
+
_settings_js_template = _SETTINGS_JS_PATH.read_text(encoding="utf-8")
|
|
91
|
+
except OSError as exc: # pragma: no cover - packaging guard
|
|
92
|
+
raise ImportError(
|
|
93
|
+
f"settings.js missing at {_SETTINGS_JS_PATH}. It must ship in "
|
|
94
|
+
"package-data (wheel), MANIFEST.in (sdist), and the PyInstaller datas "
|
|
95
|
+
"(binary) -- this is a packaging bug, not a usage error."
|
|
96
|
+
) from exc
|
|
97
|
+
# str.replace() silently no-ops on an absent token, and a *renamed* sentinel
|
|
98
|
+
# (e.g. PINNED_DEFAULTS) slips past both the "__HA_MCP_" not-in test and the
|
|
99
|
+
# esbuild/jsdom JS harness (tests/js/harness.mjs) -- `const DEFAULT_PINNED =
|
|
100
|
+
# PINNED_DEFAULTS;` is valid JS (only a runtime ReferenceError), so a drifted
|
|
101
|
+
# settings.js would ship a broken page green. Assert both sentinels are present
|
|
102
|
+
# before substituting.
|
|
103
|
+
for _sentinel in ("__HA_MCP_DEFAULT_PINNED__", "__HA_MCP_MANDATORY__"):
|
|
104
|
+
if _sentinel not in _settings_js_template:
|
|
105
|
+
raise ImportError(
|
|
106
|
+
f"settings.js is out of sync: sentinel {_sentinel} not found. "
|
|
107
|
+
"The Python injection and settings.js have drifted."
|
|
108
|
+
)
|
|
109
|
+
# sorted(), not list(): DEFAULT_PINNED_TOOLS / MANDATORY_TOOLS are sets, so
|
|
110
|
+
# json.dumps(list(...)) is per-process-ordered -- the only reason proving the
|
|
111
|
+
# original extraction byte-identical needed PYTHONHASHSEED pinned. sorted()
|
|
112
|
+
# makes the two injected arrays deterministic across processes.
|
|
113
|
+
_SETTINGS_JS = _settings_js_template.replace(
|
|
114
|
+
"__HA_MCP_DEFAULT_PINNED__", json.dumps(sorted(DEFAULT_PINNED_TOOLS))
|
|
115
|
+
).replace("__HA_MCP_MANDATORY__", json.dumps(sorted(MANDATORY_TOOLS)))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# The settings-UI CSS lives in settings.css, extracted the same way as
|
|
119
|
+
# settings.js. Unlike the JS it has no Python injection points -- a plain
|
|
120
|
+
# read, no token substitution -- and is injected inline between the same
|
|
121
|
+
# <style>/</style> tags so the served page stays byte-identical. It carries
|
|
122
|
+
# the same import-time packaging dependency as settings.js, so the same
|
|
123
|
+
# OSError -> ImportError packaging guard applies.
|
|
124
|
+
_SETTINGS_CSS_PATH = Path(__file__).parent / "settings.css"
|
|
125
|
+
try:
|
|
126
|
+
_SETTINGS_CSS = _SETTINGS_CSS_PATH.read_text(encoding="utf-8")
|
|
127
|
+
except OSError as exc: # pragma: no cover - packaging guard
|
|
128
|
+
raise ImportError(
|
|
129
|
+
f"settings.css missing at {_SETTINGS_CSS_PATH}. It must ship in "
|
|
130
|
+
"package-data (wheel), MANIFEST.in (sdist), and the PyInstaller datas "
|
|
131
|
+
"(binary) -- this is a packaging bug, not a usage error."
|
|
132
|
+
) from exc
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# The settings page HTML lives in settings.html, extracted the same way as
|
|
136
|
+
# settings.js / settings.css (a real file for editor/HTML tooling). It carries
|
|
137
|
+
# three substitution markers — two filled once at import, one per request:
|
|
138
|
+
# __HA_MCP_CSS__ -> settings.css contents (inside <style>)
|
|
139
|
+
# __HA_MCP_JS__ -> settings.js contents (inside <script>)
|
|
140
|
+
# __HA_MCP_THEME_PREFS__ -> per-request server-seeded theme prefs JSON,
|
|
141
|
+
# substituted in _render_settings_html()
|
|
142
|
+
# Same import-time packaging dependency as settings.js/css (wheel package-data,
|
|
143
|
+
# MANIFEST.in, PyInstaller datas) and the same OSError guard -- but this loader
|
|
144
|
+
# raises RuntimeError, not the ImportError that settings.js/css raise.
|
|
145
|
+
_SETTINGS_HTML_PATH = Path(__file__).parent / "settings.html"
|
|
146
|
+
try:
|
|
147
|
+
_settings_html_template = _SETTINGS_HTML_PATH.read_text(encoding="utf-8")
|
|
148
|
+
except OSError as exc: # pragma: no cover - packaging guard
|
|
149
|
+
raise RuntimeError(
|
|
150
|
+
f"settings.html missing at {_SETTINGS_HTML_PATH}. It must ship in "
|
|
151
|
+
"package-data (wheel), MANIFEST.in (sdist), and the PyInstaller datas "
|
|
152
|
+
"(binary) -- this is a packaging bug, not a usage error."
|
|
153
|
+
) from exc
|
|
154
|
+
|
|
155
|
+
# Fail fast if a marker was renamed in settings.html but not here (or vice
|
|
156
|
+
# versa) — str.replace() silently no-ops on an absent token, which would ship a
|
|
157
|
+
# page missing its CSS, JS, or server-seeded theme prefs. Assert all three
|
|
158
|
+
# markers are present.
|
|
159
|
+
for _sentinel in ("__HA_MCP_CSS__", "__HA_MCP_JS__", "__HA_MCP_THEME_PREFS__"):
|
|
160
|
+
if _sentinel not in _settings_html_template:
|
|
161
|
+
raise RuntimeError(
|
|
162
|
+
f"settings.html is out of sync: sentinel {_sentinel} not found. "
|
|
163
|
+
"The Python injection and settings.html have drifted."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# CSS and JS are injected once at import; __HA_MCP_THEME_PREFS__ remains in the
|
|
167
|
+
# string and is substituted per-request in _render_settings_html().
|
|
168
|
+
_SETTINGS_HTML = _settings_html_template.replace(
|
|
169
|
+
"__HA_MCP_CSS__", _SETTINGS_CSS
|
|
170
|
+
).replace("__HA_MCP_JS__", _SETTINGS_JS)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _build_stub_policy_handlers(*, data_dir: Path) -> dict[str, Any]:
|
|
174
|
+
"""Sidecar variant of the tool security policies handlers.
|
|
175
|
+
|
|
176
|
+
Serves policy config GET/PUT (the on-disk policy file is shared with
|
|
177
|
+
the main server), but returns 503 for pending/approve/deny — those
|
|
178
|
+
routes touch the in-memory ``ApprovalQueue`` which only exists in
|
|
179
|
+
the main server process.
|
|
180
|
+
"""
|
|
181
|
+
from pydantic import ValidationError
|
|
182
|
+
|
|
183
|
+
from ..policy.model import Policy
|
|
184
|
+
from ..policy.persistence import load_policy, save_policy
|
|
185
|
+
|
|
186
|
+
async def get_config(_: Request) -> JSONResponse:
|
|
187
|
+
try:
|
|
188
|
+
return JSONResponse(load_policy(data_dir).model_dump(mode="json"))
|
|
189
|
+
except ValueError as e:
|
|
190
|
+
# Mirror the main-server handler: surface corruption rather
|
|
191
|
+
# than crash the sidecar tab on a 500.
|
|
192
|
+
return JSONResponse(
|
|
193
|
+
{"error": str(e), "policy_file_corrupt": True},
|
|
194
|
+
status_code=500,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
async def put_config(request: Request) -> JSONResponse:
|
|
198
|
+
try:
|
|
199
|
+
new_policy = Policy.model_validate(await request.json())
|
|
200
|
+
except (ValidationError, ValueError) as e:
|
|
201
|
+
return JSONResponse({"error": str(e)}, status_code=400)
|
|
202
|
+
# Mirror main-server optimistic concurrency: reject if on-disk
|
|
203
|
+
# version moved between this caller's GET and PUT.
|
|
204
|
+
current = load_policy(data_dir)
|
|
205
|
+
if new_policy.version != current.version:
|
|
206
|
+
return JSONResponse(
|
|
207
|
+
{
|
|
208
|
+
"error": "Policy version mismatch. Reload before saving.",
|
|
209
|
+
"current_version": current.version,
|
|
210
|
+
"current_policy": current.model_dump(mode="json"),
|
|
211
|
+
},
|
|
212
|
+
status_code=409,
|
|
213
|
+
)
|
|
214
|
+
save_policy(data_dir, new_policy)
|
|
215
|
+
return JSONResponse({"saved": True, "version": new_policy.version + 1})
|
|
216
|
+
|
|
217
|
+
async def unavailable(_: Request) -> JSONResponse:
|
|
218
|
+
# 503 fires in two distinct situations:
|
|
219
|
+
# 1. The Tool Security Policies feature is turned off in
|
|
220
|
+
# addon config — the middleware never registered, so no
|
|
221
|
+
# approval queue exists.
|
|
222
|
+
# 2. The settings UI is running via the stdio sidecar — the
|
|
223
|
+
# in-memory queue lives in the main server process which
|
|
224
|
+
# isn't reachable from the sidecar.
|
|
225
|
+
# Either way, point users at the addon log for the real reason
|
|
226
|
+
# (a startup ImportError on the policy package surfaces here as
|
|
227
|
+
# the same 503 with a "ModuleNotFoundError" in the log).
|
|
228
|
+
return JSONResponse(
|
|
229
|
+
{
|
|
230
|
+
"error": (
|
|
231
|
+
"Tool security policies live approvals are not active. "
|
|
232
|
+
"Either the feature is turned off in App (add-on) config, the "
|
|
233
|
+
"settings UI is running in stdio-sidecar mode, or the "
|
|
234
|
+
"policy package failed to import at startup. Check the "
|
|
235
|
+
"App (add-on) log for ImportError / RuntimeError details if you "
|
|
236
|
+
"expected gating to be on."
|
|
237
|
+
)
|
|
238
|
+
},
|
|
239
|
+
status_code=503,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"policy_get_config": get_config,
|
|
244
|
+
"policy_put_config": put_config,
|
|
245
|
+
"policy_get_pending": unavailable,
|
|
246
|
+
"policy_post_approve": unavailable,
|
|
247
|
+
"policy_post_deny": unavailable,
|
|
248
|
+
"policy_get_tool_schema": unavailable,
|
|
249
|
+
"policy_get_value_source": unavailable,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _build_visibility_handlers(*, data_dir: Path) -> dict[str, Any]:
|
|
254
|
+
"""Settings-UI handlers for the entity visibility filter config.
|
|
255
|
+
|
|
256
|
+
Serves the on-disk ``entity_visibility.json`` GET/PUT with the same
|
|
257
|
+
optimistic-concurrency (version) guard as the tool policy handlers. Pure
|
|
258
|
+
file I/O, so it is always available (no approval-queue dependency).
|
|
259
|
+
"""
|
|
260
|
+
from pydantic import ValidationError
|
|
261
|
+
|
|
262
|
+
from ..visibility.model import VisibilityConfig
|
|
263
|
+
from ..visibility.persistence import (
|
|
264
|
+
load_visibility_config,
|
|
265
|
+
save_visibility_config,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
async def get_config(_: Request) -> JSONResponse:
|
|
269
|
+
try:
|
|
270
|
+
return JSONResponse(
|
|
271
|
+
load_visibility_config(data_dir).model_dump(mode="json")
|
|
272
|
+
)
|
|
273
|
+
except ValueError as e:
|
|
274
|
+
# Surface corruption rather than crash the tab on a 500.
|
|
275
|
+
return JSONResponse(
|
|
276
|
+
{"error": str(e), "visibility_file_corrupt": True},
|
|
277
|
+
status_code=500,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
async def put_config(request: Request) -> JSONResponse:
|
|
281
|
+
try:
|
|
282
|
+
new_config = VisibilityConfig.model_validate(await request.json())
|
|
283
|
+
except (ValidationError, ValueError) as e:
|
|
284
|
+
return JSONResponse({"error": str(e)}, status_code=400)
|
|
285
|
+
# Optimistic concurrency: reject if the on-disk version moved between
|
|
286
|
+
# this caller's GET and PUT.
|
|
287
|
+
current = load_visibility_config(data_dir)
|
|
288
|
+
if new_config.version != current.version:
|
|
289
|
+
return JSONResponse(
|
|
290
|
+
{
|
|
291
|
+
"error": (
|
|
292
|
+
"Visibility config version mismatch. Reload before saving."
|
|
293
|
+
),
|
|
294
|
+
"current_version": current.version,
|
|
295
|
+
"current_config": current.model_dump(mode="json"),
|
|
296
|
+
},
|
|
297
|
+
status_code=409,
|
|
298
|
+
)
|
|
299
|
+
save_visibility_config(data_dir, new_config)
|
|
300
|
+
return JSONResponse({"saved": True, "version": new_config.version + 1})
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
"visibility_get_config": get_config,
|
|
304
|
+
"visibility_put_config": put_config,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _render_settings_html() -> str:
|
|
309
|
+
"""Substitute the persisted theme prefs into the served settings page.
|
|
310
|
+
|
|
311
|
+
The ``server-prefs`` head script carries a ``data-prefs`` attribute
|
|
312
|
+
with a placeholder token; request-time substitution keeps
|
|
313
|
+
``_SETTINGS_HTML`` a static import-time constant (and keeps the script
|
|
314
|
+
body itself parseable at rest for the script-surface tests). Values
|
|
315
|
+
are sanitized enums / vetted hex colors and the JSON is HTML-escaped,
|
|
316
|
+
so the attribute cannot break out of its quoting context.
|
|
317
|
+
"""
|
|
318
|
+
payload = html.escape(
|
|
319
|
+
json.dumps(_load_theme_prefs(), separators=(",", ":")), quote=True
|
|
320
|
+
)
|
|
321
|
+
rendered = _SETTINGS_HTML.replace("__HA_MCP_THEME_PREFS__", payload, 1)
|
|
322
|
+
if "__HA_MCP_THEME_PREFS__" in rendered:
|
|
323
|
+
# str.replace silently no-ops when the placeholder vanishes in a
|
|
324
|
+
# refactor; the unit contract test catches that in CI, this line
|
|
325
|
+
# catches it loudly in a live deployment instead of quietly
|
|
326
|
+
# serving a page without server-side prefs.
|
|
327
|
+
logger.error(
|
|
328
|
+
"server-prefs placeholder was not substituted; theme prefs "
|
|
329
|
+
"will not survive fresh origins"
|
|
330
|
+
)
|
|
331
|
+
return rendered
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def build_settings_handlers(
|
|
335
|
+
server: HomeAssistantSmartMCPServer | None,
|
|
336
|
+
*,
|
|
337
|
+
is_sidecar: bool = False,
|
|
338
|
+
) -> dict[str, Any]:
|
|
339
|
+
"""Construct the settings UI route handlers.
|
|
340
|
+
|
|
341
|
+
When ``server`` is provided (HTTP modes), the tools list and restart
|
|
342
|
+
handler use the live FastMCP server / Supervisor client. When
|
|
343
|
+
``server`` is ``None`` (stdio sidecar process, which has no live MCP
|
|
344
|
+
server), the tools list is read from the on-disk metadata cache and
|
|
345
|
+
the restart handler returns 400 (the sidecar is not an add-on).
|
|
346
|
+
|
|
347
|
+
``is_sidecar`` forces the ``settings_info`` handler to report
|
|
348
|
+
``is_addon=False`` regardless of the inherited ``SUPERVISOR_TOKEN``
|
|
349
|
+
env var. The sidecar process inherits parent env unchanged
|
|
350
|
+
(``subprocess.Popen`` with default ``env=None``), so if the parent
|
|
351
|
+
stdio process happens to run under Supervisor (e.g. an interactive
|
|
352
|
+
debug shell inside the add-on container) the served HTML would
|
|
353
|
+
otherwise show the "Restart Add-on" button that POSTs to a route
|
|
354
|
+
the sidecar doesn't expose, surfacing as a broken UI. The sidecar
|
|
355
|
+
is *by construction* not the add-on entrypoint — pin the flag
|
|
356
|
+
accordingly.
|
|
357
|
+
|
|
358
|
+
Returns a dict mapping handler names to async Starlette handlers.
|
|
359
|
+
Both ``register_settings_routes`` (FastMCP mounting) and the stdio
|
|
360
|
+
sidecar's standalone Starlette app consume the same set of handlers
|
|
361
|
+
so the served page is identical regardless of transport.
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
async def _root_page(_: Request) -> HTMLResponse:
|
|
365
|
+
return HTMLResponse(_render_settings_html())
|
|
366
|
+
|
|
367
|
+
async def _settings_page(_: Request) -> HTMLResponse:
|
|
368
|
+
return HTMLResponse(_render_settings_html())
|
|
369
|
+
|
|
370
|
+
handlers: dict[str, Any] = {
|
|
371
|
+
"root_page": _root_page,
|
|
372
|
+
"settings_page": _settings_page,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# Tool security policies. The main server attaches an
|
|
376
|
+
# ApprovalQueue to the server object once PolicyMiddleware is wired
|
|
377
|
+
# in. Only the main server can serve the live pending/approve/deny
|
|
378
|
+
# endpoints because the queue is in-memory; the sidecar (or a main
|
|
379
|
+
# server without the queue attribute yet) falls back to stub handlers
|
|
380
|
+
# that serve config GET/PUT and return 503 for live approval routes.
|
|
381
|
+
approval_queue = (
|
|
382
|
+
getattr(server, "approval_queue", None) if server is not None else None
|
|
383
|
+
)
|
|
384
|
+
if not is_sidecar and approval_queue is not None:
|
|
385
|
+
from ..policy.handlers import build_policy_handlers
|
|
386
|
+
|
|
387
|
+
handlers.update(
|
|
388
|
+
build_policy_handlers(
|
|
389
|
+
data_dir=get_data_dir(),
|
|
390
|
+
queue=approval_queue,
|
|
391
|
+
server=server,
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
handlers.update(_build_stub_policy_handlers(data_dir=get_data_dir()))
|
|
396
|
+
|
|
397
|
+
handlers.update(_build_visibility_handlers(data_dir=get_data_dir()))
|
|
398
|
+
handlers.update(build_theme_handlers())
|
|
399
|
+
handlers.update(build_fs_handlers(server))
|
|
400
|
+
handlers.update(build_tools_handlers(server))
|
|
401
|
+
handlers.update(build_backups_handlers(server))
|
|
402
|
+
handlers.update(build_server_handlers(server, is_sidecar=is_sidecar))
|
|
403
|
+
handlers.update(build_advanced_handlers(server))
|
|
404
|
+
|
|
405
|
+
return handlers
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# Home Assistant proxies every ingress request ("Open Web UI") from the
|
|
409
|
+
# Supervisor's fixed network address. Per the add-on ingress contract
|
|
410
|
+
# (https://developers.home-assistant.io/docs/add-ons/presentation/#ingress —
|
|
411
|
+
# "Only connections from 172.30.32.2 must be allowed") the app must reject
|
|
412
|
+
# every other source. This holds under host_network too: ingress proxies to
|
|
413
|
+
# http://{app.ip_address}:{ingress_port}/, and for a host-network add-on
|
|
414
|
+
# app.ip_address is the hassio bridge gateway 172.30.32.1 — the DESTINATION the
|
|
415
|
+
# Supervisor dials (supervisor/docker/app.py ip_address(): host_network ->
|
|
416
|
+
# network.gateway). The Supervisor opens that connection from its own container
|
|
417
|
+
# address 172.30.32.2, so the transport peer the add-on sees is 172.30.32.2 for
|
|
418
|
+
# genuine ingress and some other address (a LAN host, the cloudflared tunnel at
|
|
419
|
+
# 172.30.33.x, another add-on) for a direct port-9583 hit. Verified live via
|
|
420
|
+
# netstat during an "Open Web UI" click.
|
|
421
|
+
SUPERVISOR_INGRESS_IP = "172.30.32.2"
|
|
422
|
+
|
|
423
|
+
# A settings-UI route handler: async (Request) -> Response.
|
|
424
|
+
_SettingsRoute = Callable[[Request], Awaitable[Response]]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _ingress_only(handler: _SettingsRoute) -> _SettingsRoute:
|
|
428
|
+
"""Wrap a root-mounted add-on route so only HA ingress can reach it.
|
|
429
|
+
|
|
430
|
+
Add-on root routes carry no MCP secret, so without this guard a direct
|
|
431
|
+
caller on the published port — a LAN peer, a reverse proxy / tunnel
|
|
432
|
+
forwarding the bare root, or a CSRF POST from a LAN browser — could
|
|
433
|
+
rewrite tool config, flip the tool-security-policy, or restart the
|
|
434
|
+
add-on with no authentication. We gate on the *transport* peer
|
|
435
|
+
(``request.client.host``), never ``X-Forwarded-For`` (which a caller can
|
|
436
|
+
forge). The same handlers stay reachable under ``secret_prefix``, where
|
|
437
|
+
the MCP secret path is the auth for direct/remote access.
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
@functools.wraps(handler)
|
|
441
|
+
async def _guarded(request: Request) -> Response:
|
|
442
|
+
peer = request.client.host if request.client else None
|
|
443
|
+
if peer != SUPERVISOR_INGRESS_IP:
|
|
444
|
+
logger.warning(
|
|
445
|
+
"Blocked non-ingress request to add-on root route %s from "
|
|
446
|
+
"peer %r (only the Supervisor at %s may reach root routes; "
|
|
447
|
+
"use the MCP secret path for direct/remote access).",
|
|
448
|
+
request.url.path,
|
|
449
|
+
peer,
|
|
450
|
+
SUPERVISOR_INGRESS_IP,
|
|
451
|
+
)
|
|
452
|
+
return JSONResponse(
|
|
453
|
+
{
|
|
454
|
+
"error": (
|
|
455
|
+
"This endpoint is only reachable through Home "
|
|
456
|
+
"Assistant ingress. For direct or remote access, use "
|
|
457
|
+
"the settings UI under your MCP secret path."
|
|
458
|
+
)
|
|
459
|
+
},
|
|
460
|
+
status_code=403,
|
|
461
|
+
)
|
|
462
|
+
return await handler(request)
|
|
463
|
+
|
|
464
|
+
return _guarded
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# Mount prefix the settings UI is served under in long-lived HTTP transports
|
|
468
|
+
# (Docker / standalone ha-mcp-web / OAuth / the add-on's secret-path mount).
|
|
469
|
+
# Recorded by register_settings_routes so ha_get_overview can point users at
|
|
470
|
+
# the settings page in modes that have no stdio sidecar URL file to surface
|
|
471
|
+
# (issue #1458). Stays None in pure stdio mode, where the sidecar writes
|
|
472
|
+
# ~/.ha-mcp/ui.url instead.
|
|
473
|
+
_http_settings_prefix: str | None = None
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def get_http_settings_prefix() -> str | None:
|
|
477
|
+
"""Return the settings-UI mount prefix for HTTP transports, or None.
|
|
478
|
+
|
|
479
|
+
Set by :func:`register_settings_routes` when the page is mounted on a
|
|
480
|
+
long-lived HTTP server. ``ha_get_overview`` reads it to hint at the
|
|
481
|
+
settings page when there is no stdio sidecar URL to hand the user.
|
|
482
|
+
"""
|
|
483
|
+
return _http_settings_prefix
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def register_settings_routes(
|
|
487
|
+
mcp: FastMCP,
|
|
488
|
+
server: HomeAssistantSmartMCPServer,
|
|
489
|
+
secret_path: str = "",
|
|
490
|
+
) -> None:
|
|
491
|
+
"""Register the settings UI HTTP routes on the FastMCP Starlette app.
|
|
492
|
+
|
|
493
|
+
The routes are mounted under ``secret_path`` so HTTP clients (Docker
|
|
494
|
+
/ standalone) need the same secret to reach the UI as they do to
|
|
495
|
+
reach the MCP endpoint itself — there's no native auth on FastMCP
|
|
496
|
+
custom routes (they bypass ``RequireAuthMiddleware``), so this
|
|
497
|
+
matches the auth-by-obscurity model the rest of the server uses for
|
|
498
|
+
those modes. In add-on mode (``SUPERVISOR_TOKEN`` set) the routes
|
|
499
|
+
are *also* mounted at root so HA ingress can proxy to ``localhost:9583/``
|
|
500
|
+
and serve the "Open Web UI" button. Stdio transports use a separate
|
|
501
|
+
side-process sidecar instead — see :mod:`ha_mcp.stdio_settings_sidecar`.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
mcp: The FastMCP instance to register routes on.
|
|
505
|
+
server: The HomeAssistantSmartMCPServer wrapping ``mcp``.
|
|
506
|
+
secret_path: The MCP secret path (e.g. ``/private_xxx`` or
|
|
507
|
+
``/mcp``). Required for non-add-on HTTP modes; if empty in
|
|
508
|
+
non-add-on mode, the function logs a warning and registers
|
|
509
|
+
nothing rather than expose the routes publicly.
|
|
510
|
+
"""
|
|
511
|
+
handlers = build_settings_handlers(server)
|
|
512
|
+
secret_prefix = secret_path.rstrip("/") if secret_path else ""
|
|
513
|
+
is_addon = is_running_in_addon()
|
|
514
|
+
|
|
515
|
+
if not is_addon and not secret_prefix:
|
|
516
|
+
logger.warning(
|
|
517
|
+
"register_settings_routes: not in add-on mode and no secret_path "
|
|
518
|
+
"provided — settings UI HTTP routes not registered (would otherwise "
|
|
519
|
+
"be publicly reachable). Pass MCP_SECRET_PATH or run as add-on."
|
|
520
|
+
)
|
|
521
|
+
return
|
|
522
|
+
|
|
523
|
+
# Every route this function mounts except the add-on-only root mount is defined
|
|
524
|
+
# once in this table and mounted under each active prefix below: at root
|
|
525
|
+
# in add-on mode (so HA ingress can proxy localhost:9583/), and under the
|
|
526
|
+
# secret path when one is set (Docker / standalone direct access). A
|
|
527
|
+
# deployment hits either, both, or — guarded above — neither. Deriving
|
|
528
|
+
# the mounts from one table keeps them from drifting; the frontend uses
|
|
529
|
+
# relative fetches (./api/settings/...) so the handlers work at any prefix.
|
|
530
|
+
routes: list[tuple[str, list[str], str]] = [
|
|
531
|
+
("/settings", ["GET"], "settings_page"),
|
|
532
|
+
("/api/settings/tools", ["GET"], "get_tools"),
|
|
533
|
+
("/api/settings/tools", ["POST"], "save_tools"),
|
|
534
|
+
("/api/settings/restart", ["POST"], "restart_addon"),
|
|
535
|
+
("/api/settings/info", ["GET"], "settings_info"),
|
|
536
|
+
("/api/settings/features", ["GET"], "get_feature_flags"),
|
|
537
|
+
("/api/settings/features", ["POST"], "save_feature_flags"),
|
|
538
|
+
# Theme / accessibility prefs (#1574 review) — server-side copy so
|
|
539
|
+
# they survive the stdio sidecar's per-spawn origin change
|
|
540
|
+
("/api/settings/theme", ["GET"], "get_theme_prefs"),
|
|
541
|
+
("/api/settings/theme", ["POST"], "save_theme_prefs"),
|
|
542
|
+
# Advanced settings endpoints
|
|
543
|
+
("/api/settings/advanced", ["GET"], "get_advanced_settings"),
|
|
544
|
+
("/api/settings/advanced", ["POST"], "save_advanced_settings"),
|
|
545
|
+
# Auto-backup endpoints (#1288)
|
|
546
|
+
("/api/settings/backups", ["GET"], "list_backups"),
|
|
547
|
+
("/api/settings/backups", ["DELETE"], "delete_backups_bulk"),
|
|
548
|
+
("/api/settings/backups/{name}", ["GET"], "view_backup"),
|
|
549
|
+
("/api/settings/backups/{name}/diff", ["GET"], "diff_backup"),
|
|
550
|
+
("/api/settings/backups/{name}/restore", ["POST"], "restore_backup"),
|
|
551
|
+
("/api/settings/backups/{name}", ["DELETE"], "delete_backup"),
|
|
552
|
+
("/api/settings/backup-config", ["GET"], "get_backup_config"),
|
|
553
|
+
("/api/settings/backup-config", ["POST"], "save_backup_config"),
|
|
554
|
+
# Custom filesystem directories (issue #1567) — component-owned list
|
|
555
|
+
("/api/settings/fs-custom-paths", ["GET"], "get_fs_custom_paths"),
|
|
556
|
+
("/api/settings/fs-custom-paths", ["POST"], "save_fs_custom_paths"),
|
|
557
|
+
# Tool security policies endpoints
|
|
558
|
+
("/api/policy/config", ["GET"], "policy_get_config"),
|
|
559
|
+
("/api/policy/config", ["PUT"], "policy_put_config"),
|
|
560
|
+
("/api/policy/pending", ["GET"], "policy_get_pending"),
|
|
561
|
+
("/api/policy/approve", ["POST"], "policy_post_approve"),
|
|
562
|
+
("/api/policy/deny", ["POST"], "policy_post_deny"),
|
|
563
|
+
("/api/policy/tool-schema", ["GET"], "policy_get_tool_schema"),
|
|
564
|
+
("/api/policy/value-source", ["GET"], "policy_get_value_source"),
|
|
565
|
+
# Entity visibility filter endpoints (issue #1728)
|
|
566
|
+
("/api/visibility/config", ["GET"], "visibility_get_config"),
|
|
567
|
+
("/api/visibility/config", ["PUT"], "visibility_put_config"),
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
def _mount(prefix: str, *, guard: bool = False) -> None:
|
|
571
|
+
# guard=True wraps each handler in _ingress_only so the route only
|
|
572
|
+
# answers HA ingress (the Supervisor) — used for the add-on root
|
|
573
|
+
# mount, whose port 9583 is reachable without the MCP secret.
|
|
574
|
+
for path, methods, handler_key in routes:
|
|
575
|
+
handler = handlers[handler_key]
|
|
576
|
+
if guard:
|
|
577
|
+
handler = _ingress_only(handler)
|
|
578
|
+
mcp.custom_route(f"{prefix}{path}", methods=methods)(handler)
|
|
579
|
+
|
|
580
|
+
if is_addon:
|
|
581
|
+
# Root mount lets HA ingress proxy localhost:9583/ → the settings UI
|
|
582
|
+
# ("Open Web UI" button). The published port 9583 also makes these
|
|
583
|
+
# routes reachable by direct callers that present no MCP secret, so
|
|
584
|
+
# the root mount is gated with _ingress_only: only the Supervisor
|
|
585
|
+
# (HA ingress, 172.30.32.2) may reach root; every other caller gets
|
|
586
|
+
# 403 and must use the secret-path mount below. The "Open Web UI"
|
|
587
|
+
# button is unaffected — its traffic arrives from the Supervisor.
|
|
588
|
+
mcp.custom_route("/", methods=["GET"])(_ingress_only(handlers["root_page"]))
|
|
589
|
+
_mount("", guard=True)
|
|
590
|
+
|
|
591
|
+
if secret_prefix:
|
|
592
|
+
# Mount under the MCP secret path so Docker / standalone clients
|
|
593
|
+
# need the same secret to reach the UI as they do for the MCP
|
|
594
|
+
# endpoint.
|
|
595
|
+
_mount(secret_prefix)
|
|
596
|
+
# Record the mount so ha_get_overview can point users at the settings
|
|
597
|
+
# page in HTTP transports that have no stdio sidecar URL file (#1458).
|
|
598
|
+
global _http_settings_prefix
|
|
599
|
+
_http_settings_prefix = secret_prefix
|