ha-mcp-dev 7.13.0.dev2002__tar.gz → 7.13.0.dev2005__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.dev2002/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev2005}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/pyproject.toml +1 -1
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/__main__.py +215 -14
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/__init__.py +51 -6
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_handlers_advanced.py +2 -2
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/stdio_settings_sidecar.py +13 -1
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_bug_report.py +18 -9
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_search.py +5 -1
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_system.py +5 -1
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/util_helpers.py +7 -2
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/setup.cfg +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/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.13.0.
|
|
7
|
+
version = "7.13.0.dev2005"
|
|
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
|
|
@@ -25,6 +25,7 @@ import hashlib # noqa: E402
|
|
|
25
25
|
import ipaddress # noqa: E402
|
|
26
26
|
import logging # noqa: E402
|
|
27
27
|
import os # noqa: E402
|
|
28
|
+
import secrets # noqa: E402
|
|
28
29
|
import signal # noqa: E402
|
|
29
30
|
import stat # noqa: E402
|
|
30
31
|
import sys # noqa: E402
|
|
@@ -1050,14 +1051,43 @@ def register_browser_landing(
|
|
|
1050
1051
|
)
|
|
1051
1052
|
|
|
1052
1053
|
|
|
1053
|
-
def _log_settings_url(
|
|
1054
|
+
def _log_settings_url(
|
|
1055
|
+
host: str,
|
|
1056
|
+
port: int,
|
|
1057
|
+
path: str,
|
|
1058
|
+
*,
|
|
1059
|
+
level: int = logging.INFO,
|
|
1060
|
+
note_autogen: bool = False,
|
|
1061
|
+
base_url: str | None = None,
|
|
1062
|
+
) -> None:
|
|
1054
1063
|
"""Log the web settings-UI URL at HTTP startup.
|
|
1055
1064
|
|
|
1056
1065
|
Non-add-on operators (Docker / standalone) otherwise have no easy way to
|
|
1057
1066
|
discover the settings page or its secret-path URL (issue #1458). When the
|
|
1058
1067
|
bind host is the wildcard (``0.0.0.0`` / ``::``) the process can't know its
|
|
1059
1068
|
externally reachable address, so we log a ``<host>`` placeholder.
|
|
1069
|
+
|
|
1070
|
+
``level`` and ``note_autogen`` let the OAuth/OIDC dedicated-secret path log
|
|
1071
|
+
an auto-generated URL at WARNING (visible even when internet-facing
|
|
1072
|
+
deployments run at ``LOG_LEVEL=WARNING``), since that path rotates each
|
|
1073
|
+
restart and is the only way for the admin to find it. ``base_url``, when
|
|
1074
|
+
given (OAuth/OIDC modes, where it is the required public front), is used
|
|
1075
|
+
verbatim instead of the local ``host:port`` — those deployments are reached
|
|
1076
|
+
through a tunnel/proxy, not the bind address.
|
|
1060
1077
|
"""
|
|
1078
|
+
if base_url:
|
|
1079
|
+
url = f"{base_url.rstrip('/')}{path.rstrip('/')}/settings"
|
|
1080
|
+
logger.log(
|
|
1081
|
+
level,
|
|
1082
|
+
f"Settings UI available at: {url}"
|
|
1083
|
+
+ (
|
|
1084
|
+
" [auto-generated, rotates each restart; "
|
|
1085
|
+
"set MCP_SETTINGS_SECRET_PATH for a stable URL]"
|
|
1086
|
+
if note_autogen
|
|
1087
|
+
else ""
|
|
1088
|
+
),
|
|
1089
|
+
)
|
|
1090
|
+
return
|
|
1061
1091
|
is_wildcard = host in ("0.0.0.0", "::")
|
|
1062
1092
|
if is_wildcard:
|
|
1063
1093
|
display_host = "<host>"
|
|
@@ -1068,7 +1098,158 @@ def _log_settings_url(host: str, port: int, path: str) -> None:
|
|
|
1068
1098
|
display_host = host
|
|
1069
1099
|
url = f"http://{display_host}:{port}{path.rstrip('/')}/settings"
|
|
1070
1100
|
note = " (substitute this server's address for <host>)" if is_wildcard else ""
|
|
1071
|
-
|
|
1101
|
+
if note_autogen:
|
|
1102
|
+
note += (
|
|
1103
|
+
" [auto-generated, rotates each restart; "
|
|
1104
|
+
"set MCP_SETTINGS_SECRET_PATH for a stable URL]"
|
|
1105
|
+
)
|
|
1106
|
+
logger.log(level, f"Settings UI available at: {url}{note}")
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
# Truthy / falsy env-var spellings for HA_MCP_DISABLE_SETTINGS_UI, matching the
|
|
1110
|
+
# parsing in stdio_settings_sidecar.py so the toggle behaves the same across
|
|
1111
|
+
# transports. A value in neither set is unrecognized: fail closed (disable) with
|
|
1112
|
+
# a warning, since this is a security kill switch and failing open on a typo
|
|
1113
|
+
# would leave an unauthenticated surface up.
|
|
1114
|
+
_SETTINGS_TRUTHY = {"1", "true", "yes", "on"}
|
|
1115
|
+
_SETTINGS_FALSY = {"0", "false", "no", "off", ""}
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _settings_ui_disabled() -> bool:
|
|
1119
|
+
"""Return True when ``HA_MCP_DISABLE_SETTINGS_UI`` turns the settings UI off.
|
|
1120
|
+
|
|
1121
|
+
Honored by every long-lived HTTP transport (standard ``ha-mcp-web`` /
|
|
1122
|
+
``ha-mcp-sse`` as well as OAuth/OIDC) and, with matching semantics, by the
|
|
1123
|
+
stdio sidecar. As a security kill switch it fails **closed**: any value that
|
|
1124
|
+
is neither a recognized truthy nor falsy spelling disables the UI and warns
|
|
1125
|
+
(so the operator learns why it vanished), rather than leaving an
|
|
1126
|
+
unauthenticated surface up on a typo. Unset — or an explicit falsy value —
|
|
1127
|
+
keeps the UI served.
|
|
1128
|
+
"""
|
|
1129
|
+
disable = os.getenv("HA_MCP_DISABLE_SETTINGS_UI", "").strip().lower()
|
|
1130
|
+
if disable in _SETTINGS_FALSY:
|
|
1131
|
+
return False
|
|
1132
|
+
if disable not in _SETTINGS_TRUTHY:
|
|
1133
|
+
logger.warning(
|
|
1134
|
+
"HA_MCP_DISABLE_SETTINGS_UI=%r is not a recognized on/off value; "
|
|
1135
|
+
"disabling the settings UI (fail-closed). Use one of %s to disable, "
|
|
1136
|
+
"or one of %s to keep it served.",
|
|
1137
|
+
disable,
|
|
1138
|
+
", ".join(sorted(_SETTINGS_TRUTHY)),
|
|
1139
|
+
", ".join(sorted(s for s in _SETTINGS_FALSY if s)),
|
|
1140
|
+
)
|
|
1141
|
+
return True
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
def _resolve_settings_secret_path() -> str | None:
|
|
1145
|
+
"""Resolve the dedicated settings-UI secret path for OAuth/OIDC modes.
|
|
1146
|
+
|
|
1147
|
+
These modes are internet-facing, so the settings UI must never sit at the
|
|
1148
|
+
guessable default MCP path, where FastMCP custom routes bypass the OAuth /
|
|
1149
|
+
OIDC auth middleware (GHSA-mx64-982r-65vg). It gets its own secret path:
|
|
1150
|
+
|
|
1151
|
+
- ``HA_MCP_DISABLE_SETTINGS_UI`` truthy → ``None`` (do not mount at all).
|
|
1152
|
+
- ``MCP_SETTINGS_SECRET_PATH`` set → that path (stable across restarts).
|
|
1153
|
+
- neither → an auto-generated ``/private_<token>`` path (logged at startup).
|
|
1154
|
+
|
|
1155
|
+
Returns the path to mount the settings UI under, or ``None`` when disabled.
|
|
1156
|
+
"""
|
|
1157
|
+
if _settings_ui_disabled():
|
|
1158
|
+
return None
|
|
1159
|
+
explicit = os.getenv("MCP_SETTINGS_SECRET_PATH", "").strip()
|
|
1160
|
+
if explicit:
|
|
1161
|
+
return explicit
|
|
1162
|
+
return "/private_" + secrets.token_urlsafe(16)
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def _register_settings_ui_secret_path(
|
|
1166
|
+
mcp: "FastMCP",
|
|
1167
|
+
server: "HomeAssistantSmartMCPServer",
|
|
1168
|
+
host: str,
|
|
1169
|
+
port: int,
|
|
1170
|
+
mcp_path: str,
|
|
1171
|
+
base_url: str,
|
|
1172
|
+
) -> None:
|
|
1173
|
+
"""Register the settings UI behind a dedicated secret path (OAuth/OIDC modes).
|
|
1174
|
+
|
|
1175
|
+
Standard HTTP mode serves the settings UI under the MCP secret path itself,
|
|
1176
|
+
but in OAuth/OIDC modes that path is known to clients and OAuth/OIDC — not
|
|
1177
|
+
path secrecy — is the auth boundary. So the settings UI gets its own secret
|
|
1178
|
+
path from :func:`_resolve_settings_secret_path`, mounted with
|
|
1179
|
+
``advertise_prefix=False`` so the path is never returned to MCP clients via
|
|
1180
|
+
``ha_get_overview``. The admin learns the URL from the startup log.
|
|
1181
|
+
|
|
1182
|
+
``mcp_path`` is the MCP endpoint path. The settings path is validated against
|
|
1183
|
+
it and rejected (fail closed — UI not mounted) if it is malformed or reuses
|
|
1184
|
+
the MCP path, either of which would re-expose the unauthenticated routes
|
|
1185
|
+
(GHSA-mx64-982r-65vg).
|
|
1186
|
+
"""
|
|
1187
|
+
from ha_mcp.settings_ui import register_settings_routes
|
|
1188
|
+
|
|
1189
|
+
settings_path = _resolve_settings_secret_path()
|
|
1190
|
+
if settings_path is None:
|
|
1191
|
+
logger.info(
|
|
1192
|
+
"Settings UI disabled (HA_MCP_DISABLE_SETTINGS_UI); routes not registered."
|
|
1193
|
+
)
|
|
1194
|
+
return
|
|
1195
|
+
|
|
1196
|
+
explicit = bool(os.getenv("MCP_SETTINGS_SECRET_PATH", "").strip())
|
|
1197
|
+
|
|
1198
|
+
# An explicit path may be malformed; the auto-generated /private_<token> is
|
|
1199
|
+
# always well-formed. Starlette asserts routes start with "/", so a missing
|
|
1200
|
+
# leading slash would crash startup — normalize it (and tell the admin).
|
|
1201
|
+
if not settings_path.startswith("/"):
|
|
1202
|
+
logger.warning(
|
|
1203
|
+
"MCP_SETTINGS_SECRET_PATH=%r has no leading '/'; normalizing to %r.",
|
|
1204
|
+
settings_path,
|
|
1205
|
+
"/" + settings_path,
|
|
1206
|
+
)
|
|
1207
|
+
settings_path = "/" + settings_path
|
|
1208
|
+
if "{" in settings_path or "}" in settings_path:
|
|
1209
|
+
# Starlette compiles {name} into a wildcard capture, so a braced path
|
|
1210
|
+
# (an unrendered /private_{token} template, or a literal paste of the
|
|
1211
|
+
# docs' /private_<token> convention) would mount the settings routes as a
|
|
1212
|
+
# publicly-matching wildcard — the exact exposure this guards against.
|
|
1213
|
+
logger.error(
|
|
1214
|
+
"MCP_SETTINGS_SECRET_PATH (%r) contains route-template characters "
|
|
1215
|
+
"('{{'/'}}'), which Starlette would turn into a wildcard route, "
|
|
1216
|
+
"re-exposing the settings UI (GHSA-mx64-982r-65vg). Use a literal path "
|
|
1217
|
+
"or unset it to auto-generate; settings UI not registered.",
|
|
1218
|
+
settings_path,
|
|
1219
|
+
)
|
|
1220
|
+
return
|
|
1221
|
+
if not settings_path.rstrip("/"):
|
|
1222
|
+
logger.error(
|
|
1223
|
+
"MCP_SETTINGS_SECRET_PATH must be a non-empty absolute path like "
|
|
1224
|
+
"'/private_xxx'; settings UI not registered."
|
|
1225
|
+
)
|
|
1226
|
+
return
|
|
1227
|
+
if settings_path.rstrip("/") == mcp_path.rstrip("/"):
|
|
1228
|
+
logger.error(
|
|
1229
|
+
"MCP_SETTINGS_SECRET_PATH (%r) must not equal the MCP path (%r): the "
|
|
1230
|
+
"settings routes bypass OAuth/OIDC auth, so reusing the client-known MCP "
|
|
1231
|
+
"path would re-expose them (GHSA-mx64-982r-65vg). Set a distinct path or "
|
|
1232
|
+
"unset it to auto-generate; settings UI not registered.",
|
|
1233
|
+
settings_path,
|
|
1234
|
+
mcp_path,
|
|
1235
|
+
)
|
|
1236
|
+
return
|
|
1237
|
+
|
|
1238
|
+
register_settings_routes(
|
|
1239
|
+
mcp, server, secret_path=settings_path, advertise_prefix=False
|
|
1240
|
+
)
|
|
1241
|
+
# Auto-generated paths rotate each restart and are the only way the admin can
|
|
1242
|
+
# find the UI, so log them at WARNING to survive LOG_LEVEL=WARNING; an
|
|
1243
|
+
# explicit path the admin already knows stays at INFO. OAuth/OIDC are fronted
|
|
1244
|
+
# by MCP_BASE_URL (a public HTTPS tunnel/proxy), so log that, not host:port.
|
|
1245
|
+
_log_settings_url(
|
|
1246
|
+
host,
|
|
1247
|
+
port,
|
|
1248
|
+
settings_path,
|
|
1249
|
+
level=logging.INFO if explicit else logging.WARNING,
|
|
1250
|
+
note_autogen=not explicit,
|
|
1251
|
+
base_url=base_url,
|
|
1252
|
+
)
|
|
1072
1253
|
|
|
1073
1254
|
|
|
1074
1255
|
def _run_http_server(transport: str, default_port: int = 8086) -> None:
|
|
@@ -1089,8 +1270,13 @@ def _run_http_server(transport: str, default_port: int = 8086) -> None:
|
|
|
1089
1270
|
register_browser_landing(_get_mcp(), path, quiet_probe_log=transport != "sse")
|
|
1090
1271
|
if _healthz_enabled():
|
|
1091
1272
|
_register_healthz_route(_get_mcp())
|
|
1092
|
-
|
|
1093
|
-
|
|
1273
|
+
if _settings_ui_disabled():
|
|
1274
|
+
logger.info(
|
|
1275
|
+
"Settings UI disabled (HA_MCP_DISABLE_SETTINGS_UI); routes not registered."
|
|
1276
|
+
)
|
|
1277
|
+
else:
|
|
1278
|
+
register_settings_routes(_get_mcp(), _get_server(), secret_path=path)
|
|
1279
|
+
_log_settings_url(host, port, path)
|
|
1094
1280
|
|
|
1095
1281
|
_run_entrypoint(
|
|
1096
1282
|
_run_http_with_graceful_shutdown(transport, host, port, path),
|
|
@@ -1107,6 +1293,8 @@ def main_web() -> None:
|
|
|
1107
1293
|
- MCP_HOST (optional, default: "0.0.0.0"; set 127.0.0.1 to restrict to loopback)
|
|
1108
1294
|
- MCP_PORT (optional, default: 8086)
|
|
1109
1295
|
- MCP_SECRET_PATH (optional, default: "/mcp")
|
|
1296
|
+
- HA_MCP_DISABLE_SETTINGS_UI (optional): set truthy to not serve the web
|
|
1297
|
+
settings UI at all
|
|
1110
1298
|
"""
|
|
1111
1299
|
_setup_standard_mode()
|
|
1112
1300
|
_run_http_server("http", default_port=8086)
|
|
@@ -1121,6 +1309,8 @@ def main_sse() -> None:
|
|
|
1121
1309
|
- MCP_HOST (optional, default: "0.0.0.0"; set 127.0.0.1 to restrict to loopback)
|
|
1122
1310
|
- MCP_PORT (optional, default: 8087)
|
|
1123
1311
|
- MCP_SECRET_PATH (optional, default: "/mcp")
|
|
1312
|
+
- HA_MCP_DISABLE_SETTINGS_UI (optional): set truthy to not serve the web
|
|
1313
|
+
settings UI at all
|
|
1124
1314
|
"""
|
|
1125
1315
|
_setup_standard_mode()
|
|
1126
1316
|
_run_http_server("sse", default_port=8087)
|
|
@@ -1138,7 +1328,12 @@ def main_oauth() -> None:
|
|
|
1138
1328
|
- MCP_BASE_URL (required): Public URL where this server is accessible (e.g., https://your-tunnel.com)
|
|
1139
1329
|
- MCP_HOST (optional, default: "0.0.0.0"; set 127.0.0.1 to restrict to loopback)
|
|
1140
1330
|
- MCP_PORT (optional, default: 8086)
|
|
1141
|
-
- MCP_SECRET_PATH (optional, default: "/mcp")
|
|
1331
|
+
- MCP_SECRET_PATH (optional, default: "/mcp") — the MCP endpoint path only;
|
|
1332
|
+
the settings UI uses its own secret path (below), not this one
|
|
1333
|
+
- MCP_SETTINGS_SECRET_PATH (optional): dedicated secret path for the web
|
|
1334
|
+
settings UI. Auto-generated and logged at startup when unset.
|
|
1335
|
+
- HA_MCP_DISABLE_SETTINGS_UI (optional): set truthy to not serve the
|
|
1336
|
+
settings UI at all
|
|
1142
1337
|
- LOG_LEVEL (optional, default: INFO)
|
|
1143
1338
|
|
|
1144
1339
|
Note: HOMEASSISTANT_TOKEN is NOT required in this mode.
|
|
@@ -1243,10 +1438,9 @@ async def _run_oauth_server(
|
|
|
1243
1438
|
if _healthz_enabled():
|
|
1244
1439
|
_register_healthz_route(mcp)
|
|
1245
1440
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
_log_settings_url(host, port, path)
|
|
1441
|
+
# The settings UI must not sit at the OAuth-known MCP path (custom routes
|
|
1442
|
+
# bypass the OAuth auth middleware); mount it behind a dedicated secret path.
|
|
1443
|
+
_register_settings_ui_secret_path(mcp, _server, host, port, path, base_url)
|
|
1250
1444
|
|
|
1251
1445
|
tools = await mcp.list_tools()
|
|
1252
1446
|
logger.info(
|
|
@@ -1294,7 +1488,12 @@ def main_oidc() -> None:
|
|
|
1294
1488
|
- MCP_HOST (optional, default: "0.0.0.0"; set 127.0.0.1 to restrict to loopback
|
|
1295
1489
|
behind a same-host reverse proxy)
|
|
1296
1490
|
- MCP_PORT (optional, default: 8086)
|
|
1297
|
-
- MCP_SECRET_PATH (optional, default: "/mcp")
|
|
1491
|
+
- MCP_SECRET_PATH (optional, default: "/mcp") — the MCP endpoint path only;
|
|
1492
|
+
the settings UI uses its own secret path (below), not this one
|
|
1493
|
+
- MCP_SETTINGS_SECRET_PATH (optional): dedicated secret path for the web
|
|
1494
|
+
settings UI. Auto-generated and logged at startup when unset.
|
|
1495
|
+
- HA_MCP_DISABLE_SETTINGS_UI (optional): set truthy to not serve the
|
|
1496
|
+
settings UI at all
|
|
1298
1497
|
- LOG_LEVEL (optional, default: INFO)
|
|
1299
1498
|
"""
|
|
1300
1499
|
# Configure logging for OIDC mode (force=True needed since logging may already be configured)
|
|
@@ -1432,10 +1631,12 @@ async def _run_oidc_server(
|
|
|
1432
1631
|
mcp_instance.auth = auth
|
|
1433
1632
|
|
|
1434
1633
|
logger.info("Server created with OIDC authentication")
|
|
1435
|
-
#
|
|
1436
|
-
# browser
|
|
1437
|
-
#
|
|
1438
|
-
#
|
|
1634
|
+
# OIDC mode intentionally registers no browser landing page: an
|
|
1635
|
+
# unauthenticated browser GET gets a bare 405 rather than a friendly page.
|
|
1636
|
+
# The settings UI *is* served, but only behind a dedicated secret path
|
|
1637
|
+
# (never the OIDC-known MCP path, whose custom routes bypass the auth
|
|
1638
|
+
# middleware — GHSA-mx64-982r-65vg).
|
|
1639
|
+
_register_settings_ui_secret_path(mcp_instance, _server, host, port, path, base_url)
|
|
1439
1640
|
if _healthz_enabled():
|
|
1440
1641
|
_register_healthz_route(mcp_instance)
|
|
1441
1642
|
logger.info(f"Starting OIDC-enabled MCP server at {base_url}{path}")
|
|
@@ -520,16 +520,34 @@ def get_http_settings_prefix() -> str | None:
|
|
|
520
520
|
"""Return the settings-UI mount prefix for HTTP transports, or None.
|
|
521
521
|
|
|
522
522
|
Set by :func:`register_settings_routes` when the page is mounted on a
|
|
523
|
-
long-lived HTTP server. ``ha_get_overview``
|
|
524
|
-
settings page when there is no stdio sidecar URL to
|
|
523
|
+
long-lived HTTP server *and* advertising is enabled. ``ha_get_overview``
|
|
524
|
+
reads it to hint at the settings page when there is no stdio sidecar URL to
|
|
525
|
+
hand the user. It is None when the mount is deliberately not advertised
|
|
526
|
+
(OAuth/OIDC dedicated-secret mode) — use :func:`is_http_settings_mounted`,
|
|
527
|
+
not this, to tell HTTP from the stdio sidecar.
|
|
525
528
|
"""
|
|
526
529
|
return _http_settings_prefix
|
|
527
530
|
|
|
528
531
|
|
|
532
|
+
# Whether the settings UI is served over HTTP (add-on / Docker / OAuth / OIDC)
|
|
533
|
+
# as opposed to the stdio sidecar. Distinct from _http_settings_prefix, which is
|
|
534
|
+
# the *advertised* URL and is None when a mount is deliberately hidden from MCP
|
|
535
|
+
# clients (advertise_prefix=False). Consumers that only need "is this stdio?"
|
|
536
|
+
# must read this, not the prefix (GHSA-mx64-982r-65vg fix left the prefix None
|
|
537
|
+
# on a real HTTP mount).
|
|
538
|
+
_http_settings_mounted: bool = False
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def is_http_settings_mounted() -> bool:
|
|
542
|
+
"""Return True when the settings UI is HTTP-mounted (not the stdio sidecar)."""
|
|
543
|
+
return _http_settings_mounted
|
|
544
|
+
|
|
545
|
+
|
|
529
546
|
def register_settings_routes(
|
|
530
547
|
mcp: FastMCP,
|
|
531
548
|
server: HomeAssistantSmartMCPServer,
|
|
532
549
|
secret_path: str = "",
|
|
550
|
+
advertise_prefix: bool = True,
|
|
533
551
|
) -> None:
|
|
534
552
|
"""Register the settings UI HTTP routes on the FastMCP Starlette app.
|
|
535
553
|
|
|
@@ -550,9 +568,27 @@ def register_settings_routes(
|
|
|
550
568
|
``/mcp``). Required for non-add-on HTTP modes; if empty in
|
|
551
569
|
non-add-on mode, the function logs a warning and registers
|
|
552
570
|
nothing rather than expose the routes publicly.
|
|
571
|
+
advertise_prefix: When True (default), record the secret-path mount in
|
|
572
|
+
``_http_settings_prefix`` so ``ha_get_overview`` can hint at the
|
|
573
|
+
settings URL. OAuth/OIDC modes pass False: there the settings UI
|
|
574
|
+
sits under a *dedicated* secret path that must never be handed to
|
|
575
|
+
MCP clients (GHSA-mx64-982r-65vg), so the mount happens but the
|
|
576
|
+
prefix is not recorded.
|
|
553
577
|
"""
|
|
554
578
|
handlers = build_settings_handlers(server)
|
|
555
579
|
secret_prefix = secret_path.rstrip("/") if secret_path else ""
|
|
580
|
+
if secret_prefix and ("{" in secret_prefix or "}" in secret_prefix):
|
|
581
|
+
# Starlette compiles {name} into a [^/]+ wildcard capture, so a braced
|
|
582
|
+
# secret path (e.g. an unrendered /private_{token} template) would mount
|
|
583
|
+
# these routes at a publicly-matching wildcard. Drop the secret mount
|
|
584
|
+
# rather than expose it (GHSA-mx64-982r-65vg); the add-on root mount below
|
|
585
|
+
# is unaffected.
|
|
586
|
+
logger.error(
|
|
587
|
+
"settings UI secret path %r contains route-template characters "
|
|
588
|
+
"('{{'/'}}'); not mounting under it (it would become a wildcard route).",
|
|
589
|
+
secret_prefix,
|
|
590
|
+
)
|
|
591
|
+
secret_prefix = ""
|
|
556
592
|
is_addon = is_running_in_addon()
|
|
557
593
|
|
|
558
594
|
if not is_addon and not secret_prefix:
|
|
@@ -563,6 +599,12 @@ def register_settings_routes(
|
|
|
563
599
|
)
|
|
564
600
|
return
|
|
565
601
|
|
|
602
|
+
# Past this point at least one HTTP mount happens (add-on root and/or the
|
|
603
|
+
# secret path). Record that so consumers can distinguish HTTP from the stdio
|
|
604
|
+
# sidecar even when the prefix itself is not advertised (advertise_prefix=False).
|
|
605
|
+
global _http_settings_mounted
|
|
606
|
+
_http_settings_mounted = True
|
|
607
|
+
|
|
566
608
|
# Every route this function mounts except the add-on-only root mount is defined
|
|
567
609
|
# once in this table and mounted under each active prefix below: at root
|
|
568
610
|
# in add-on mode (so HA ingress can proxy localhost:9583/), and under the
|
|
@@ -636,7 +678,10 @@ def register_settings_routes(
|
|
|
636
678
|
# need the same secret to reach the UI as they do for the MCP
|
|
637
679
|
# endpoint.
|
|
638
680
|
_mount(secret_prefix)
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
681
|
+
if advertise_prefix:
|
|
682
|
+
# Record the mount so ha_get_overview can point users at the
|
|
683
|
+
# settings page in HTTP transports that have no stdio sidecar URL
|
|
684
|
+
# file (#1458). Suppressed in OAuth/OIDC modes, where the dedicated
|
|
685
|
+
# secret path must not leak to MCP clients (GHSA-mx64-982r-65vg).
|
|
686
|
+
global _http_settings_prefix
|
|
687
|
+
_http_settings_prefix = secret_prefix
|
{ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/settings_ui/_handlers_advanced.py
RENAMED
|
@@ -136,7 +136,7 @@ async def _get_advanced_settings(
|
|
|
136
136
|
and saves route through Supervisor so the add-on Configuration tab and
|
|
137
137
|
the web UI share state.
|
|
138
138
|
"""
|
|
139
|
-
from ha_mcp.settings_ui import
|
|
139
|
+
from ha_mcp.settings_ui import is_http_settings_mounted
|
|
140
140
|
|
|
141
141
|
from ..config import ADVANCED_SETTINGS_FIELDS, _read_feature_flag_override_file
|
|
142
142
|
|
|
@@ -172,7 +172,7 @@ async def _get_advanced_settings(
|
|
|
172
172
|
{
|
|
173
173
|
"fields": fields,
|
|
174
174
|
"is_addon": is_running_in_addon(),
|
|
175
|
-
"is_stdio":
|
|
175
|
+
"is_stdio": not is_http_settings_mounted(),
|
|
176
176
|
}
|
|
177
177
|
)
|
|
178
178
|
|
{ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/stdio_settings_sidecar.py
RENAMED
|
@@ -61,6 +61,7 @@ logger = logging.getLogger(__name__)
|
|
|
61
61
|
# importing from the tools subpackage because this module is loaded
|
|
62
62
|
# during early stdio startup, before any tool code is touched.
|
|
63
63
|
_TRUTHY = {"1", "true", "yes", "on"}
|
|
64
|
+
_FALSY = {"0", "false", "no", "off", ""}
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
def _sidecar_dir() -> Path:
|
|
@@ -109,7 +110,18 @@ def read_sidecar_url() -> str | None:
|
|
|
109
110
|
|
|
110
111
|
def _is_disabled() -> bool:
|
|
111
112
|
"""Check whether the sidecar should be skipped."""
|
|
112
|
-
|
|
113
|
+
val = os.environ.get("HA_MCP_DISABLE_SETTINGS_UI", "").strip().lower()
|
|
114
|
+
if val in _TRUTHY:
|
|
115
|
+
return True
|
|
116
|
+
if val and val not in _FALSY:
|
|
117
|
+
# Fail closed on an unrecognized value (same semantics as the HTTP
|
|
118
|
+
# transports' _settings_ui_disabled), and warn so the operator sees why
|
|
119
|
+
# the settings UI never came up.
|
|
120
|
+
logger.warning(
|
|
121
|
+
"HA_MCP_DISABLE_SETTINGS_UI=%r is not a recognized on/off value; "
|
|
122
|
+
"skipping the settings-UI sidecar (fail-closed).",
|
|
123
|
+
val,
|
|
124
|
+
)
|
|
113
125
|
return True
|
|
114
126
|
return _disabled_sentinel().exists()
|
|
115
127
|
|
{ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/tools/tools_bug_report.py
RENAMED
|
@@ -443,15 +443,24 @@ def _sanitize_log_text(text: str) -> str:
|
|
|
443
443
|
# configured value (catches custom paths) and the generated
|
|
444
444
|
# ``/private_<token>`` convention. Only bug-report output is scrubbed here;
|
|
445
445
|
# the raw logs this text is copied from are left intact.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
)
|
|
446
|
+
# The dedicated settings-UI secret path (OAuth/OIDC) is a second secret whose
|
|
447
|
+
# leak grants the same unauthenticated settings access, so redact a custom
|
|
448
|
+
# value the same way. The auto-generated /private_<token> form is caught by
|
|
449
|
+
# the generic pattern below.
|
|
450
|
+
for env_name in ("MCP_SECRET_PATH", "MCP_SETTINGS_SECRET_PATH"):
|
|
451
|
+
# Strip before rstrip: the resolver strips MCP_SETTINGS_SECRET_PATH before
|
|
452
|
+
# mounting, so a whitespace-bearing value mounts at the stripped path — the
|
|
453
|
+
# redaction pattern must match that, not the raw value (GHSA-mx64-982r-65vg).
|
|
454
|
+
configured = os.getenv(env_name, "").strip().rstrip("/")
|
|
455
|
+
if configured and configured != "/mcp":
|
|
456
|
+
# Anchor on a path-segment boundary so a short/substring-prone
|
|
457
|
+
# configured value (e.g. "/ha") cannot corrupt unrelated text
|
|
458
|
+
# (e.g. "/happy").
|
|
459
|
+
text = re.sub(
|
|
460
|
+
re.escape(configured) + r"(?![A-Za-z0-9_-])",
|
|
461
|
+
"[REDACTED_SECRET_PATH]",
|
|
462
|
+
text,
|
|
463
|
+
)
|
|
455
464
|
text = re.sub(r"/private_[A-Za-z0-9_-]+", "[REDACTED_SECRET_PATH]", text)
|
|
456
465
|
return text
|
|
457
466
|
|
|
@@ -3449,7 +3449,11 @@ class SearchTools:
|
|
|
3449
3449
|
)
|
|
3450
3450
|
result["repair_count"] = len(visible_issues)
|
|
3451
3451
|
if not include_dismissed_repairs_bool:
|
|
3452
|
-
|
|
3452
|
+
# Baseline excludes inactive registry stubs so they are
|
|
3453
|
+
# not miscounted as dismissed.
|
|
3454
|
+
dismissed_count = len(
|
|
3455
|
+
filter_active_repairs(all_issues, include_dismissed=True)
|
|
3456
|
+
) - len(visible_issues)
|
|
3453
3457
|
if dismissed_count:
|
|
3454
3458
|
result["dismissed_repair_count"] = dismissed_count
|
|
3455
3459
|
result["repairs"] = [project_repair_fields(r) for r in visible_issues]
|
|
@@ -1225,7 +1225,11 @@ class SystemTools:
|
|
|
1225
1225
|
"count": len(visible_issues),
|
|
1226
1226
|
}
|
|
1227
1227
|
if not include_dismissed:
|
|
1228
|
-
|
|
1228
|
+
# Baseline excludes inactive registry stubs so they are
|
|
1229
|
+
# not miscounted as dismissed.
|
|
1230
|
+
dismissed_count = len(
|
|
1231
|
+
filter_active_repairs(all_issues, include_dismissed=True)
|
|
1232
|
+
) - len(visible_issues)
|
|
1229
1233
|
if dismissed_count:
|
|
1230
1234
|
repairs["dismissed_count"] = dismissed_count
|
|
1231
1235
|
else:
|
|
@@ -567,10 +567,15 @@ def filter_active_repairs(
|
|
|
567
567
|
Repairs UI hides ignored ones by default). Mirror that UI default so
|
|
568
568
|
overview / system-health responses don't surface repairs the user has
|
|
569
569
|
already dismissed.
|
|
570
|
+
|
|
571
|
+
Entries with ``active=False`` are always dropped, regardless of
|
|
572
|
+
``include_dismissed``: HA core's ``ws_list_issues`` never emits them
|
|
573
|
+
(the issue registry keeps inactive stubs for every non-persistent issue
|
|
574
|
+
it has ever stored), but the component's raw registry dump does.
|
|
570
575
|
"""
|
|
571
576
|
if include_dismissed:
|
|
572
|
-
return
|
|
573
|
-
return [r for r in issues if not r.get("ignored")]
|
|
577
|
+
return [r for r in issues if r.get("active") is not False]
|
|
578
|
+
return [r for r in issues if r.get("active") is not False and not r.get("ignored")]
|
|
574
579
|
|
|
575
580
|
|
|
576
581
|
def project_repair_fields(issue: dict[str, Any]) -> dict[str, Any]:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/client/supervisor_client.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/client/websocket_client.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/client/websocket_listener.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/dashboard_screenshot/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/dashboard_screenshot/capture.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/dashboard_screenshot/content.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/dashboard_screenshot/paths.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.13.0.dev2002 → ha_mcp_dev-7.13.0.dev2005}/src/ha_mcp/dashboard_screenshot/provision.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|