ha-mcp-dev 7.13.0.dev1973__tar.gz → 7.13.0.dev1977__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.13.0.dev1973/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev1977}/PKG-INFO +2 -1
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/README.md +1 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/pyproject.toml +2 -1
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/__main__.py +220 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/backup_manager.py +63 -21
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/backup.py +149 -20
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/component_api.py +85 -16
- ha_mcp_dev-7.13.0.dev1977/src/ha_mcp/tools/component_config_reads.py +212 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/component_devices.py +41 -6
- ha_mcp_dev-7.13.0.dev1977/src/ha_mcp/tools/component_registries.py +144 -0
- ha_mcp_dev-7.13.0.dev1977/src/ha_mcp/tools/component_registry_lookup.py +161 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/base.py +12 -3
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/reference_validator.py +24 -4
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_areas.py +80 -55
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_blueprints.py +12 -1
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_config_automations.py +31 -2
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_config_dashboards.py +768 -16
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_config_helpers.py +58 -8
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_config_scenes.py +71 -4
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_dev.py +133 -16
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_entities.py +13 -3
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_integrations.py +413 -58
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_search.py +158 -40
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_services.py +114 -5
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_system.py +293 -28
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_voice_assistant.py +16 -4
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_zones.py +19 -3
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/util_helpers.py +31 -8
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/visibility/model.py +27 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/visibility/resolver.py +39 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977/src/ha_mcp_dev.egg-info}/PKG-INFO +2 -1
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp_dev.egg-info/SOURCES.txt +3 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp_dev.egg-info/entry_points.txt +1 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/setup.cfg +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.13.0.dev1973 → ha_mcp_dev-7.13.0.dev1977}/tests/test_env_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ha-mcp-dev
|
|
3
|
-
Version: 7.13.0.
|
|
3
|
+
Version: 7.13.0.dev1977
|
|
4
4
|
Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
|
|
5
5
|
Author-email: Julien <github@qc-h.net>
|
|
6
6
|
License: MIT
|
|
@@ -118,6 +118,7 @@ These run the server outside Home Assistant — useful for **Container** / **Cor
|
|
|
118
118
|
- **Docker (HTTP server):** run `ghcr.io/homeassistant-ai/ha-mcp` in HTTP mode, pointed at your Home Assistant URL and a long-lived token, and connect your client to its secret URL. See the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) for the full command and per-client config.
|
|
119
119
|
- **PyPI / uvx (HTTP server):** run the published `ha-mcp` package with `uvx ha-mcp@latest` (or pip) as a streamable-HTTP server the same way. Details in the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/).
|
|
120
120
|
- **Local stdio (not recommended):** runs ha-mcp on your own machine over stdio. The one-command installers in the **Demo server** section below use this path; the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) covers connecting it to your own Home Assistant.
|
|
121
|
+
- **OIDC authentication:** gate remote access behind an external identity provider (Authentik, Keycloak, Auth0, Google, etc.) instead of a secret URL — all authenticated users share the server's Home Assistant credentials. See [OIDC Mode](docs/oidc.md).
|
|
121
122
|
|
|
122
123
|
> ⚠️ **stdio has known transport issues.** The stdio transport has connection problems that streamable HTTP does not ([#1713](https://github.com/homeassistant-ai/ha-mcp/issues/1713)). It is recommended only for demo/testing tinkering — for a real setup, use the custom component or an HTTP method above.
|
|
123
124
|
|
|
@@ -85,6 +85,7 @@ These run the server outside Home Assistant — useful for **Container** / **Cor
|
|
|
85
85
|
- **Docker (HTTP server):** run `ghcr.io/homeassistant-ai/ha-mcp` in HTTP mode, pointed at your Home Assistant URL and a long-lived token, and connect your client to its secret URL. See the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) for the full command and per-client config.
|
|
86
86
|
- **PyPI / uvx (HTTP server):** run the published `ha-mcp` package with `uvx ha-mcp@latest` (or pip) as a streamable-HTTP server the same way. Details in the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/).
|
|
87
87
|
- **Local stdio (not recommended):** runs ha-mcp on your own machine over stdio. The one-command installers in the **Demo server** section below use this path; the [Setup Wizard](https://homeassistant-ai.github.io/ha-mcp/setup/) covers connecting it to your own Home Assistant.
|
|
88
|
+
- **OIDC authentication:** gate remote access behind an external identity provider (Authentik, Keycloak, Auth0, Google, etc.) instead of a secret URL — all authenticated users share the server's Home Assistant credentials. See [OIDC Mode](docs/oidc.md).
|
|
88
89
|
|
|
89
90
|
> ⚠️ **stdio has known transport issues.** The stdio transport has connection problems that streamable HTTP does not ([#1713](https://github.com/homeassistant-ai/ha-mcp/issues/1713)). It is recommended only for demo/testing tinkering — for a real setup, use the custom component or an HTTP method above.
|
|
90
91
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.13.0.
|
|
7
|
+
version = "7.13.0.dev1977"
|
|
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
|
|
@@ -59,6 +59,7 @@ hamcp-test-env = "tests.test_env_manager:main"
|
|
|
59
59
|
ha-mcp-web = "ha_mcp.__main__:main_web"
|
|
60
60
|
ha-mcp-sse = "ha_mcp.__main__:main_sse"
|
|
61
61
|
ha-mcp-oauth = "ha_mcp.__main__:main_oauth"
|
|
62
|
+
ha-mcp-oidc = "ha_mcp.__main__:main_oidc"
|
|
62
63
|
|
|
63
64
|
[tool.setuptools]
|
|
64
65
|
package-dir = {"" = "src", "tests" = "tests"}
|
|
@@ -979,6 +979,39 @@ def _healthz_enabled() -> bool:
|
|
|
979
979
|
return os.getenv("MCP_HEALTHZ", "").strip().lower() in ("1", "true", "yes", "on")
|
|
980
980
|
|
|
981
981
|
|
|
982
|
+
def _oidc_verify_id_token_enabled() -> bool:
|
|
983
|
+
"""True when OIDC_VERIFY_ID_TOKEN opts in to ID-token verification.
|
|
984
|
+
|
|
985
|
+
Off by default: FastMCP's OIDCProxy verifies the access token as a JWT,
|
|
986
|
+
which works for providers like Authentik and Keycloak. Providers that
|
|
987
|
+
issue opaque access tokens (Google always; Auth0 without an API
|
|
988
|
+
audience) need ``verify_id_token=True`` so FastMCP verifies the ID token
|
|
989
|
+
instead.
|
|
990
|
+
"""
|
|
991
|
+
return os.getenv("OIDC_VERIFY_ID_TOKEN", "").strip().lower() in (
|
|
992
|
+
"1",
|
|
993
|
+
"true",
|
|
994
|
+
"yes",
|
|
995
|
+
"on",
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _oidc_allowed_client_redirect_uris() -> list[str] | None:
|
|
1000
|
+
"""Parse OIDC_ALLOWED_CLIENT_REDIRECT_URIS into a list, or None if unset.
|
|
1001
|
+
|
|
1002
|
+
Comma-separated list of redirect URI patterns FastMCP's OIDCProxy will
|
|
1003
|
+
accept from dynamically-registered clients. With no allow-list set,
|
|
1004
|
+
FastMCP still validates a client's redirect URI against that same
|
|
1005
|
+
client's own registered redirect URIs — the exposure is that open DCR
|
|
1006
|
+
lets an attacker register their own client with their own redirect URI;
|
|
1007
|
+
internet-facing deployments should set this to constrain what any
|
|
1008
|
+
dynamically-registered client may register in the first place.
|
|
1009
|
+
"""
|
|
1010
|
+
raw = os.getenv("OIDC_ALLOWED_CLIENT_REDIRECT_URIS", "")
|
|
1011
|
+
uris = [uri.strip() for uri in raw.split(",") if uri.strip()]
|
|
1012
|
+
return uris or None
|
|
1013
|
+
|
|
1014
|
+
|
|
982
1015
|
def register_browser_landing(
|
|
983
1016
|
mcp_instance: "FastMCP | _DeferredMCP",
|
|
984
1017
|
path: str,
|
|
@@ -1225,5 +1258,192 @@ async def _run_oauth_server(
|
|
|
1225
1258
|
)
|
|
1226
1259
|
|
|
1227
1260
|
|
|
1261
|
+
def main_oidc() -> None:
|
|
1262
|
+
"""Run server with OIDC authentication over HTTP.
|
|
1263
|
+
|
|
1264
|
+
This mode enables authentication via an external OIDC provider
|
|
1265
|
+
(Authentik, Keycloak, Auth0, Google, etc.). All authenticated users
|
|
1266
|
+
share the same Home Assistant instance via the configured credentials.
|
|
1267
|
+
|
|
1268
|
+
Unlike OAuth mode which collects per-user HA credentials via a consent form,
|
|
1269
|
+
OIDC mode is purely an access gate — users authenticate through the OIDC provider,
|
|
1270
|
+
then all requests use the shared HA credentials from environment variables.
|
|
1271
|
+
|
|
1272
|
+
Environment:
|
|
1273
|
+
- OIDC_CONFIG_URL (required): OIDC discovery URL (.well-known/openid-configuration)
|
|
1274
|
+
- OIDC_CLIENT_ID (required): OAuth client ID registered with your OIDC provider
|
|
1275
|
+
- OIDC_CLIENT_SECRET (required): OAuth client secret from your OIDC provider
|
|
1276
|
+
- MCP_BASE_URL (required): Public HTTPS URL where this server is accessible
|
|
1277
|
+
- HOMEASSISTANT_URL (required): Home Assistant instance URL
|
|
1278
|
+
- HOMEASSISTANT_TOKEN (required): Home Assistant long-lived access token or supervisor token
|
|
1279
|
+
- OIDC_JWT_SIGNING_KEY (optional): Secret key for signing FastMCP JWTs. Sessions
|
|
1280
|
+
persist across restarts by default (the key is derived from OIDC_CLIENT_SECRET);
|
|
1281
|
+
set this to decouple the signing key from the client secret. Generate with:
|
|
1282
|
+
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
|
1283
|
+
- OIDC_ALLOWED_CLIENT_REDIRECT_URIS (optional): Comma-separated list of redirect URI
|
|
1284
|
+
patterns accepted from dynamically-registered clients. Strongly recommended for
|
|
1285
|
+
internet-facing deployments.
|
|
1286
|
+
- OIDC_VERIFY_ID_TOKEN (optional, default: false): Set true for providers that issue
|
|
1287
|
+
opaque access tokens (e.g. Google, or Auth0 without an API audience).
|
|
1288
|
+
- OIDC_AUDIENCE (optional): Expected `aud` claim for IdP-issued access tokens.
|
|
1289
|
+
Without it (and with OIDC_VERIFY_ID_TOKEN off), FastMCP's JWT verifier checks
|
|
1290
|
+
issuer, signature, and expiry but not audience. With OIDC_VERIFY_ID_TOKEN=true,
|
|
1291
|
+
verification pins `aud` to OIDC_CLIENT_ID instead and this value is only
|
|
1292
|
+
forwarded to the IdP's authorize/token endpoints (it is what makes Auth0
|
|
1293
|
+
issue JWT rather than opaque access tokens).
|
|
1294
|
+
- MCP_HOST (optional, default: "0.0.0.0"; set 127.0.0.1 to restrict to loopback
|
|
1295
|
+
behind a same-host reverse proxy)
|
|
1296
|
+
- MCP_PORT (optional, default: 8086)
|
|
1297
|
+
- MCP_SECRET_PATH (optional, default: "/mcp")
|
|
1298
|
+
- LOG_LEVEL (optional, default: INFO)
|
|
1299
|
+
"""
|
|
1300
|
+
# Configure logging for OIDC mode (force=True needed since logging may already be configured)
|
|
1301
|
+
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
1302
|
+
_setup_logging(log_level, force=True)
|
|
1303
|
+
for logger_name in ["ha_mcp", "ha_mcp.auth", "ha_mcp.auth.provider"]:
|
|
1304
|
+
logging.getLogger(logger_name).setLevel(getattr(logging, log_level))
|
|
1305
|
+
logger.info(f"OIDC mode logging configured at {log_level} level")
|
|
1306
|
+
_log_startup_version()
|
|
1307
|
+
|
|
1308
|
+
# Validate HA credentials (OIDC mode needs them — unlike OAuth mode)
|
|
1309
|
+
from ha_mcp.config import get_settings
|
|
1310
|
+
|
|
1311
|
+
settings = get_settings()
|
|
1312
|
+
_validate_standard_credentials(settings)
|
|
1313
|
+
|
|
1314
|
+
# Validate OIDC-specific env vars
|
|
1315
|
+
oidc_config_url = os.getenv("OIDC_CONFIG_URL")
|
|
1316
|
+
oidc_client_id = os.getenv("OIDC_CLIENT_ID")
|
|
1317
|
+
oidc_client_secret = os.getenv("OIDC_CLIENT_SECRET")
|
|
1318
|
+
base_url = os.getenv("MCP_BASE_URL")
|
|
1319
|
+
|
|
1320
|
+
missing = []
|
|
1321
|
+
if not oidc_config_url:
|
|
1322
|
+
missing.append("OIDC_CONFIG_URL")
|
|
1323
|
+
if not oidc_client_id:
|
|
1324
|
+
missing.append("OIDC_CLIENT_ID")
|
|
1325
|
+
if not oidc_client_secret:
|
|
1326
|
+
missing.append("OIDC_CLIENT_SECRET")
|
|
1327
|
+
if not base_url:
|
|
1328
|
+
missing.append("MCP_BASE_URL")
|
|
1329
|
+
|
|
1330
|
+
if missing:
|
|
1331
|
+
logger.error(
|
|
1332
|
+
f"Missing required environment variables for OIDC mode: {', '.join(missing)}"
|
|
1333
|
+
)
|
|
1334
|
+
logger.error(
|
|
1335
|
+
"OIDC mode requires an external OIDC provider (Authentik, Keycloak, Auth0, etc.)"
|
|
1336
|
+
)
|
|
1337
|
+
sys.exit(1)
|
|
1338
|
+
|
|
1339
|
+
assert oidc_config_url is not None
|
|
1340
|
+
assert oidc_client_id is not None
|
|
1341
|
+
assert oidc_client_secret is not None
|
|
1342
|
+
assert base_url is not None
|
|
1343
|
+
|
|
1344
|
+
host, port, path = _get_http_runtime(default_port=8086)
|
|
1345
|
+
|
|
1346
|
+
_run_entrypoint(
|
|
1347
|
+
_run_oidc_server(
|
|
1348
|
+
config_url=oidc_config_url,
|
|
1349
|
+
client_id=oidc_client_id,
|
|
1350
|
+
client_secret=oidc_client_secret,
|
|
1351
|
+
base_url=base_url,
|
|
1352
|
+
host=host,
|
|
1353
|
+
port=port,
|
|
1354
|
+
path=path,
|
|
1355
|
+
),
|
|
1356
|
+
"OIDC server",
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
async def _run_oidc_server(
|
|
1361
|
+
config_url: str,
|
|
1362
|
+
client_id: str,
|
|
1363
|
+
client_secret: str,
|
|
1364
|
+
base_url: str,
|
|
1365
|
+
host: str,
|
|
1366
|
+
port: int,
|
|
1367
|
+
path: str,
|
|
1368
|
+
) -> None:
|
|
1369
|
+
"""Run the OIDC-authenticated MCP server.
|
|
1370
|
+
|
|
1371
|
+
Unlike OAuth mode which uses OAuthProxyClient for per-user credential routing,
|
|
1372
|
+
OIDC mode uses the standard HomeAssistantClient with shared credentials.
|
|
1373
|
+
OIDC is purely an access gate — all authenticated users share the same HA instance.
|
|
1374
|
+
|
|
1375
|
+
Args:
|
|
1376
|
+
config_url: OIDC discovery URL (.well-known/openid-configuration)
|
|
1377
|
+
client_id: OAuth client ID from the OIDC provider
|
|
1378
|
+
client_secret: OAuth client secret from the OIDC provider
|
|
1379
|
+
base_url: Public HTTPS URL where this server is accessible
|
|
1380
|
+
host: Bind host (typically 0.0.0.0; override via MCP_HOST)
|
|
1381
|
+
port: Port to listen on
|
|
1382
|
+
path: MCP endpoint path
|
|
1383
|
+
"""
|
|
1384
|
+
from fastmcp.server.auth.oidc_proxy import OIDCProxy
|
|
1385
|
+
|
|
1386
|
+
from ha_mcp.server import HomeAssistantSmartMCPServer
|
|
1387
|
+
from ha_mcp.transport_security import ensure_host_origin_guard_default_off
|
|
1388
|
+
|
|
1389
|
+
# OIDC mode is deployed behind a public reverse proxy or LAN hostname;
|
|
1390
|
+
# disable FastMCP's Host/Origin rebinding guard so proxied requests are
|
|
1391
|
+
# not rejected with 421/403 before OIDC auth runs.
|
|
1392
|
+
ensure_host_origin_guard_default_off()
|
|
1393
|
+
|
|
1394
|
+
proxy_kwargs: dict[str, Any] = {
|
|
1395
|
+
"config_url": config_url,
|
|
1396
|
+
"client_id": client_id,
|
|
1397
|
+
"client_secret": client_secret,
|
|
1398
|
+
"base_url": base_url,
|
|
1399
|
+
# "external" tells FastMCP that consent is handled by the upstream
|
|
1400
|
+
# IdP (Authentik, Keycloak, etc.) -- unlike `False`, this does not
|
|
1401
|
+
# log a security warning at startup that consent is disabled.
|
|
1402
|
+
"require_authorization_consent": "external",
|
|
1403
|
+
# Preserve `or None`: an empty-but-set env var must not bypass
|
|
1404
|
+
# FastMCP's derive-from-client-secret default for jwt_signing_key.
|
|
1405
|
+
"jwt_signing_key": os.getenv("OIDC_JWT_SIGNING_KEY") or None,
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
allowed_redirect_uris = _oidc_allowed_client_redirect_uris()
|
|
1409
|
+
if allowed_redirect_uris:
|
|
1410
|
+
proxy_kwargs["allowed_client_redirect_uris"] = allowed_redirect_uris
|
|
1411
|
+
else:
|
|
1412
|
+
logger.warning(
|
|
1413
|
+
"OIDC_ALLOWED_CLIENT_REDIRECT_URIS is not set: any dynamically-registered "
|
|
1414
|
+
"(DCR) client's own redirect URIs are accepted. Internet-facing "
|
|
1415
|
+
"deployments should set it."
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
if _oidc_verify_id_token_enabled():
|
|
1419
|
+
proxy_kwargs["verify_id_token"] = True
|
|
1420
|
+
|
|
1421
|
+
audience = os.getenv("OIDC_AUDIENCE", "").strip()
|
|
1422
|
+
if audience:
|
|
1423
|
+
proxy_kwargs["audience"] = audience
|
|
1424
|
+
|
|
1425
|
+
# Create OIDC auth provider — auto-discovers endpoints from config_url
|
|
1426
|
+
auth = OIDCProxy(**proxy_kwargs)
|
|
1427
|
+
|
|
1428
|
+
# Standard server with shared credentials (no proxy client needed)
|
|
1429
|
+
global _server
|
|
1430
|
+
_server = HomeAssistantSmartMCPServer()
|
|
1431
|
+
mcp_instance = _server.mcp
|
|
1432
|
+
mcp_instance.auth = auth
|
|
1433
|
+
|
|
1434
|
+
logger.info("Server created with OIDC authentication")
|
|
1435
|
+
# Unlike standard/OAuth HTTP modes, OIDC mode intentionally registers no
|
|
1436
|
+
# browser landing page and no settings-UI sidecar: an unauthenticated
|
|
1437
|
+
# browser GET gets a bare 405 rather than a friendly page, and there is
|
|
1438
|
+
# no settings UI surface here.
|
|
1439
|
+
if _healthz_enabled():
|
|
1440
|
+
_register_healthz_route(mcp_instance)
|
|
1441
|
+
logger.info(f"Starting OIDC-enabled MCP server at {base_url}{path}")
|
|
1442
|
+
|
|
1443
|
+
await _run_with_shutdown(
|
|
1444
|
+
mcp_instance.run_async(**_http_run_kwargs("http", host, port, path))
|
|
1445
|
+
)
|
|
1446
|
+
|
|
1447
|
+
|
|
1228
1448
|
if __name__ == "__main__":
|
|
1229
1449
|
main()
|
|
@@ -1296,14 +1296,18 @@ async def _restore_scene(client: Any, entity_id: str, config: Any) -> Any:
|
|
|
1296
1296
|
async def _fetch_dashboard(client: Any, entity_id: str) -> Any:
|
|
1297
1297
|
"""Fetch a dashboard config via the same helper the get tool uses.
|
|
1298
1298
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1299
|
+
The identifier is pre-resolved to its canonical url_path via the shared
|
|
1300
|
+
``_resolve_dashboard`` (component ``list`` when available), then the config is
|
|
1301
|
+
read through the component ``get`` (one in-process frame) with a fall back to
|
|
1302
|
+
the legacy ``lovelace/config`` read (``_get_dashboard_config_internal``, which
|
|
1303
|
+
handles the WS envelope, force-cache-bypass, and structured error wrapping).
|
|
1304
|
+
The component refuses YAML bodies, so those capture through legacy unchanged.
|
|
1305
|
+
Imported lazily to avoid an import cycle.
|
|
1303
1306
|
"""
|
|
1304
1307
|
from fastmcp.exceptions import ToolError
|
|
1305
1308
|
|
|
1306
1309
|
from .tools.tools_config_dashboards import (
|
|
1310
|
+
_component_dashboard_config,
|
|
1307
1311
|
_get_dashboard_config_internal,
|
|
1308
1312
|
_resolve_dashboard,
|
|
1309
1313
|
)
|
|
@@ -1329,6 +1333,12 @@ async def _fetch_dashboard(client: Any, entity_id: str) -> Any:
|
|
|
1329
1333
|
err,
|
|
1330
1334
|
)
|
|
1331
1335
|
|
|
1336
|
+
# Component fast path (freshness-safe in-memory read); None ⇒ legacy below,
|
|
1337
|
+
# which also covers YAML dashboards and not-found (nothing to back up).
|
|
1338
|
+
component_config = await _component_dashboard_config(client, fetch_path)
|
|
1339
|
+
if component_config is not None:
|
|
1340
|
+
return component_config
|
|
1341
|
+
|
|
1332
1342
|
try:
|
|
1333
1343
|
config, _config_hash = await _get_dashboard_config_internal(client, fetch_path)
|
|
1334
1344
|
except ToolError as err:
|
|
@@ -1442,10 +1452,21 @@ def _strip_readonly(config: dict[str, Any], *extra: str) -> dict[str, Any]:
|
|
|
1442
1452
|
|
|
1443
1453
|
|
|
1444
1454
|
async def _fetch_label(client: Any, entity_id: str) -> Any:
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
)
|
|
1455
|
+
# Route the capture through the component's ``registries`` capability when
|
|
1456
|
+
# available (one in-process read of the label registry) instead of dumping
|
|
1457
|
+
# the whole registry via WS. Lazy import to avoid the backup_manager →
|
|
1458
|
+
# tools → backup_manager cycle (same pattern as ``_fetch_device``). ``None``
|
|
1459
|
+
# from the helper means "component unavailable"; fall back to the full list.
|
|
1460
|
+
from .tools.component_registries import fetch_registries_via_component
|
|
1461
|
+
|
|
1462
|
+
component_result = await fetch_registries_via_component(client, ["label"])
|
|
1463
|
+
if component_result is not None:
|
|
1464
|
+
items = component_result.get("labels") or []
|
|
1465
|
+
else:
|
|
1466
|
+
items = _require_list(
|
|
1467
|
+
await _ws_send(client, {"type": "config/label_registry/list"}),
|
|
1468
|
+
"config/label_registry/list",
|
|
1469
|
+
)
|
|
1449
1470
|
for item in items:
|
|
1450
1471
|
if item.get("label_id") == entity_id:
|
|
1451
1472
|
return item
|
|
@@ -1466,12 +1487,22 @@ async def _fetch_category(client: Any, entity_id: str) -> Any:
|
|
|
1466
1487
|
scope, _, cat_id = entity_id.partition(":")
|
|
1467
1488
|
if not cat_id:
|
|
1468
1489
|
return None
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1490
|
+
# Same component-first routing as ``_fetch_label``; categories are scoped,
|
|
1491
|
+
# so the requested scope rides ``category_scopes``.
|
|
1492
|
+
from .tools.component_registries import fetch_registries_via_component
|
|
1493
|
+
|
|
1494
|
+
component_result = await fetch_registries_via_component(
|
|
1495
|
+
client, ["category"], category_scopes=[scope]
|
|
1474
1496
|
)
|
|
1497
|
+
if component_result is not None:
|
|
1498
|
+
items = (component_result.get("categories") or {}).get(scope, [])
|
|
1499
|
+
else:
|
|
1500
|
+
items = _require_list(
|
|
1501
|
+
await _ws_send(
|
|
1502
|
+
client, {"type": "config/category_registry/list", "scope": scope}
|
|
1503
|
+
),
|
|
1504
|
+
"config/category_registry/list",
|
|
1505
|
+
)
|
|
1475
1506
|
for item in items:
|
|
1476
1507
|
if item.get("category_id") == cat_id:
|
|
1477
1508
|
return {"scope": scope, **item}
|
|
@@ -1607,19 +1638,30 @@ async def _fetch_area_or_floor(client: Any, entity_id: str) -> Any:
|
|
|
1607
1638
|
kind, _, real_id = entity_id.partition(":")
|
|
1608
1639
|
if not real_id:
|
|
1609
1640
|
return None
|
|
1641
|
+
# Same component-first routing as ``_fetch_label`` / ``_fetch_category``.
|
|
1642
|
+
from .tools.component_registries import fetch_registries_via_component
|
|
1643
|
+
|
|
1610
1644
|
if kind == "area":
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
"
|
|
1614
|
-
|
|
1645
|
+
component_result = await fetch_registries_via_component(client, ["area"])
|
|
1646
|
+
if component_result is not None:
|
|
1647
|
+
items = component_result.get("areas") or []
|
|
1648
|
+
else:
|
|
1649
|
+
items = _require_list(
|
|
1650
|
+
await _ws_send(client, {"type": "config/area_registry/list"}),
|
|
1651
|
+
"config/area_registry/list",
|
|
1652
|
+
)
|
|
1615
1653
|
for item in items:
|
|
1616
1654
|
if item.get("area_id") == real_id:
|
|
1617
1655
|
return {"kind": "area", **item}
|
|
1618
1656
|
elif kind == "floor":
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
"
|
|
1622
|
-
|
|
1657
|
+
component_result = await fetch_registries_via_component(client, ["floor"])
|
|
1658
|
+
if component_result is not None:
|
|
1659
|
+
items = component_result.get("floors") or []
|
|
1660
|
+
else:
|
|
1661
|
+
items = _require_list(
|
|
1662
|
+
await _ws_send(client, {"type": "config/floor_registry/list"}),
|
|
1663
|
+
"config/floor_registry/list",
|
|
1664
|
+
)
|
|
1623
1665
|
for item in items:
|
|
1624
1666
|
if item.get("floor_id") == real_id:
|
|
1625
1667
|
return {"kind": "floor", **item}
|
|
@@ -30,10 +30,21 @@ from ..backup_manager import (
|
|
|
30
30
|
MandatoryBackupError,
|
|
31
31
|
get_backup_manager,
|
|
32
32
|
)
|
|
33
|
-
from ..client.rest_client import
|
|
34
|
-
|
|
33
|
+
from ..client.rest_client import (
|
|
34
|
+
HomeAssistantClient,
|
|
35
|
+
HomeAssistantCommandError,
|
|
36
|
+
HomeAssistantCommandTimeout,
|
|
37
|
+
HomeAssistantError,
|
|
38
|
+
)
|
|
39
|
+
from ..client.websocket_client import HomeAssistantWebSocketClient, get_websocket_client
|
|
35
40
|
from ..config import get_global_settings
|
|
36
41
|
from ..errors import ErrorCode, create_error_response
|
|
42
|
+
from .component_api import (
|
|
43
|
+
component_supports,
|
|
44
|
+
get_component_caps,
|
|
45
|
+
invalidate_caps,
|
|
46
|
+
is_unknown_command,
|
|
47
|
+
)
|
|
37
48
|
from .helpers import (
|
|
38
49
|
exception_to_structured_error,
|
|
39
50
|
get_connected_ws_client,
|
|
@@ -89,6 +100,9 @@ _BACKUP_PROGRESS_INTERVAL_S = 10
|
|
|
89
100
|
# _wait_timeout) can be too tight for a slow or rate-limited remote agent —
|
|
90
101
|
# same class of false-timeout problem this file already fixes for creation.
|
|
91
102
|
_BACKUP_DELETE_WAIT_S = 60.0
|
|
103
|
+
# WS command name is a module-local constant per the component-routing seam
|
|
104
|
+
# pattern (see component_api.py / component_devices.py).
|
|
105
|
+
WS_BACKUP_PREP = "ha_mcp_tools/backup_prep"
|
|
92
106
|
|
|
93
107
|
|
|
94
108
|
def _get_backup_hint_text() -> str:
|
|
@@ -217,6 +231,91 @@ async def _get_backup_password(
|
|
|
217
231
|
return cast(str, default_password)
|
|
218
232
|
|
|
219
233
|
|
|
234
|
+
async def _backup_prep_via_component(
|
|
235
|
+
client: HomeAssistantClient,
|
|
236
|
+
) -> dict[str, Any] | None:
|
|
237
|
+
"""One ``ha_mcp_tools/backup_prep`` read; ``None`` ⇒ run the legacy two-call path.
|
|
238
|
+
|
|
239
|
+
Returns the component's ``{agent_ids, local_agent_id, default_password}``
|
|
240
|
+
payload, replacing the sequential ``backup/agents/info`` (local-agent
|
|
241
|
+
discovery, :func:`_get_local_backup_agent_id`) + ``backup/config/info``
|
|
242
|
+
(default password, :func:`_get_backup_password`) WS round-trips with one
|
|
243
|
+
in-process read. ``None`` on capability miss, downgrade (``unknown_command``
|
|
244
|
+
→ invalidate the cached caps), or command error/timeout (logged) — the
|
|
245
|
+
caller falls back to the legacy sequential calls. A
|
|
246
|
+
``HomeAssistantConnectionError`` (pooled-WS drop) or the plain ``Exception``
|
|
247
|
+
``get_websocket_client()`` raises on a failed (re)connect is caught here and
|
|
248
|
+
mapped to ``None``: the legacy probes (``_get_local_backup_agent_id`` /
|
|
249
|
+
``_get_backup_password``) run on a DEDICATED ``get_connected_ws_client`` socket
|
|
250
|
+
already connected before this read, NOT this pooled one — so a wedged pooled
|
|
251
|
+
socket must fall back to the legacy calls rather than fail create/restore.
|
|
252
|
+
Same caps-gate discipline as ``component_devices.fetch_device_via_component``.
|
|
253
|
+
"""
|
|
254
|
+
caps = await get_component_caps(client)
|
|
255
|
+
if not component_supports(caps, "backup_prep"):
|
|
256
|
+
return None
|
|
257
|
+
try:
|
|
258
|
+
ws = await get_websocket_client(url=client.base_url, token=client.token)
|
|
259
|
+
raw = await ws.send_command(WS_BACKUP_PREP)
|
|
260
|
+
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
261
|
+
if is_unknown_command(exc):
|
|
262
|
+
invalidate_caps(client)
|
|
263
|
+
else:
|
|
264
|
+
logger.warning("%s failed; fell back to legacy: %r", WS_BACKUP_PREP, exc)
|
|
265
|
+
return None
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
# HomeAssistantConnectionError (pooled-WS drop) OR the plain Exception
|
|
268
|
+
# get_websocket_client() raises on a failed (re)connect. The legacy probes
|
|
269
|
+
# ride a dedicated already-connected socket, so fall back rather than fail
|
|
270
|
+
# create/restore on a wedged pooled socket.
|
|
271
|
+
logger.warning(
|
|
272
|
+
"%s connection error; falling back to legacy: %r", WS_BACKUP_PREP, exc
|
|
273
|
+
)
|
|
274
|
+
return None
|
|
275
|
+
result = raw.get("result")
|
|
276
|
+
if not isinstance(result, dict) or "local_agent_id" not in result:
|
|
277
|
+
logger.debug(
|
|
278
|
+
"%s returned a malformed result (missing 'local_agent_id' key); "
|
|
279
|
+
"falling back to legacy",
|
|
280
|
+
WS_BACKUP_PREP,
|
|
281
|
+
)
|
|
282
|
+
return None
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _raise_no_local_backup_agent_error(agent_ids: list[Any] | None) -> NoReturn:
|
|
287
|
+
"""Same "no local agent" error ``_get_local_backup_agent_id`` raises.
|
|
288
|
+
|
|
289
|
+
Used when the ``backup_prep`` component read comes back with
|
|
290
|
+
``local_agent_id: None`` — the SAME outcome as the legacy probe finding no
|
|
291
|
+
agent named ``"local"``, so both paths fail identically.
|
|
292
|
+
"""
|
|
293
|
+
raise_tool_error(
|
|
294
|
+
create_error_response(
|
|
295
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
296
|
+
"No local backup agent found",
|
|
297
|
+
context={"available_agents": [a for a in (agent_ids or []) if a]},
|
|
298
|
+
suggestions=[
|
|
299
|
+
"Backup creation requires a local agent (hassio.local on "
|
|
300
|
+
"Supervised, backup.local on Core); none is registered",
|
|
301
|
+
],
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _raise_no_default_password_error() -> NoReturn:
|
|
307
|
+
"""Same error ``_get_backup_password`` raises when no default password is configured."""
|
|
308
|
+
raise_tool_error(
|
|
309
|
+
create_error_response(
|
|
310
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
311
|
+
"No default backup password configured in Home Assistant",
|
|
312
|
+
suggestions=[
|
|
313
|
+
"Configure automatic backups in Home Assistant settings to set a default password"
|
|
314
|
+
],
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
220
319
|
def _parse_backup_date(raw: Any) -> datetime | None:
|
|
221
320
|
"""Parse an HA backup `date` (ISO-8601, may use `Z` suffix) to a tz-aware
|
|
222
321
|
datetime, returning None on missing/malformed input. Naive timestamps are
|
|
@@ -530,13 +629,24 @@ async def create_backup(
|
|
|
530
629
|
)
|
|
531
630
|
ws_client = cast(HomeAssistantWebSocketClient, ws_client)
|
|
532
631
|
|
|
533
|
-
#
|
|
534
|
-
|
|
632
|
+
# One component read replaces the sequential password + local-agent
|
|
633
|
+
# probes when the ha_mcp_tools component supports backup_prep.
|
|
634
|
+
prep = await _backup_prep_via_component(client)
|
|
635
|
+
if prep is not None:
|
|
636
|
+
password = prep.get("default_password")
|
|
637
|
+
if not password:
|
|
638
|
+
_raise_no_default_password_error()
|
|
639
|
+
local_agent = prep.get("local_agent_id")
|
|
640
|
+
if not local_agent:
|
|
641
|
+
_raise_no_local_backup_agent_error(prep.get("agent_ids"))
|
|
642
|
+
else:
|
|
643
|
+
# Get backup password (raises ToolError on failure)
|
|
644
|
+
password = await _get_backup_password(ws_client)
|
|
535
645
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
646
|
+
# Discover the local backup agent at call time. HA Core registers
|
|
647
|
+
# `backup.local`; HA Supervised registers `hassio.local`. Hardcoding
|
|
648
|
+
# either breaks the other deployment.
|
|
649
|
+
local_agent = await _get_local_backup_agent_id(ws_client)
|
|
540
650
|
|
|
541
651
|
# Generate backup name if not provided
|
|
542
652
|
if not name:
|
|
@@ -768,19 +878,38 @@ async def restore_backup(
|
|
|
768
878
|
)
|
|
769
879
|
)
|
|
770
880
|
|
|
771
|
-
#
|
|
772
|
-
#
|
|
773
|
-
|
|
774
|
-
|
|
881
|
+
# One component read replaces the sequential local-agent + password
|
|
882
|
+
# probes when the ha_mcp_tools component supports backup_prep.
|
|
883
|
+
prep = await _backup_prep_via_component(client)
|
|
884
|
+
if prep is not None:
|
|
885
|
+
# Local backup agent (Supervisor's hassio.local on Supervised,
|
|
886
|
+
# backup.local on Core). Used for both the safety backup and the
|
|
887
|
+
# restore call below.
|
|
888
|
+
local_agent = prep.get("local_agent_id")
|
|
889
|
+
if not local_agent:
|
|
890
|
+
_raise_no_local_backup_agent_error(prep.get("agent_ids"))
|
|
891
|
+
|
|
892
|
+
# Create safety backup BEFORE restoring
|
|
893
|
+
logger.info("Creating safety backup before restore...")
|
|
894
|
+
password = prep.get("default_password")
|
|
895
|
+
if not password:
|
|
896
|
+
# No default password - log warning but continue (restore might still work)
|
|
897
|
+
logger.warning("No default password - proceeding without safety backup")
|
|
898
|
+
password = None
|
|
899
|
+
else:
|
|
900
|
+
# Discover the local backup agent (Supervisor's hassio.local on
|
|
901
|
+
# Supervised, backup.local on Core). Used for both the safety backup
|
|
902
|
+
# and the restore call below.
|
|
903
|
+
local_agent = await _get_local_backup_agent_id(ws_client)
|
|
775
904
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
905
|
+
# Create safety backup BEFORE restoring
|
|
906
|
+
logger.info("Creating safety backup before restore...")
|
|
907
|
+
try:
|
|
908
|
+
password = await _get_backup_password(ws_client)
|
|
909
|
+
except ToolError:
|
|
910
|
+
# Password error - log warning but continue (restore might still work)
|
|
911
|
+
logger.warning("No default password - proceeding without safety backup")
|
|
912
|
+
password = None
|
|
784
913
|
|
|
785
914
|
safety_backup_id, safety_warnings = await _create_safety_backup(
|
|
786
915
|
ws_client, password, local_agent, ctx=ctx
|