ha-mcp-dev 8.3.0.dev2385__tar.gz → 8.3.0.dev2387__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-8.3.0.dev2385/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.3.0.dev2387}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/pyproject.toml +1 -1
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_addons.py +285 -66
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/setup.cfg +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/renamed_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/component_config_entries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/config_entry_identity.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/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 = "8.3.0.
|
|
7
|
+
version = "8.3.0.dev2387"
|
|
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
|
|
@@ -11,6 +11,7 @@ import asyncio
|
|
|
11
11
|
import json
|
|
12
12
|
import logging
|
|
13
13
|
import re
|
|
14
|
+
import ssl
|
|
14
15
|
import time
|
|
15
16
|
from typing import Annotated, Any, ClassVar, Literal, NoReturn
|
|
16
17
|
from urllib.parse import unquote, urlsplit
|
|
@@ -23,7 +24,11 @@ from ha_mcp._vendor import websockets
|
|
|
23
24
|
from ha_mcp._vendor.websockets.asyncio.client import ClientConnection
|
|
24
25
|
|
|
25
26
|
from .._version import is_running_in_addon
|
|
26
|
-
from ..client.rest_client import
|
|
27
|
+
from ..client.rest_client import (
|
|
28
|
+
HomeAssistantClient,
|
|
29
|
+
HomeAssistantCommandError,
|
|
30
|
+
_is_ssl_error,
|
|
31
|
+
)
|
|
27
32
|
from ..errors import (
|
|
28
33
|
ErrorCode,
|
|
29
34
|
create_error_response,
|
|
@@ -892,6 +897,111 @@ async def _collect_ws_messages_loop(
|
|
|
892
897
|
total_size += len(clean)
|
|
893
898
|
|
|
894
899
|
|
|
900
|
+
def _tls_setup_error(e: Exception, slug: str) -> NoReturn:
|
|
901
|
+
"""Report a CA-store/TLS-context build failure distinctly from network errors."""
|
|
902
|
+
raise_tool_error(
|
|
903
|
+
create_error_response(
|
|
904
|
+
ErrorCode.CONNECTION_FAILED,
|
|
905
|
+
f"Could not build a TLS context for the app (add-on) request: {e!s}",
|
|
906
|
+
context={"slug": slug},
|
|
907
|
+
suggestions=[
|
|
908
|
+
"The MCP host's CA store failed to load. Check SSL_CERT_FILE "
|
|
909
|
+
"and SSL_CERT_DIR, or the system certificate bundle.",
|
|
910
|
+
],
|
|
911
|
+
)
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _build_ws_ssl_context(
|
|
916
|
+
ws_url: str, verify_ssl: bool, slug: str
|
|
917
|
+
) -> ssl.SSLContext | None:
|
|
918
|
+
"""Build the proxy's client TLS context, honoring the HA verify setting."""
|
|
919
|
+
if not ws_url.startswith("wss://"):
|
|
920
|
+
return None
|
|
921
|
+
try:
|
|
922
|
+
ssl_context = ssl.create_default_context()
|
|
923
|
+
except OSError as e:
|
|
924
|
+
_tls_setup_error(e, slug)
|
|
925
|
+
if not verify_ssl:
|
|
926
|
+
logger.warning(
|
|
927
|
+
"TLS verification disabled for add-on WebSocket proxy "
|
|
928
|
+
"(HA_VERIFY_SSL=false). Connecting to %s with hostname/cert "
|
|
929
|
+
"checks off.",
|
|
930
|
+
ws_url,
|
|
931
|
+
)
|
|
932
|
+
ssl_context.check_hostname = False
|
|
933
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
934
|
+
return ssl_context
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def _tls_verification_failure_suggestions() -> list[str]:
|
|
938
|
+
"""Name the TLS remedy instead of pointing the caller at the network."""
|
|
939
|
+
return [
|
|
940
|
+
"Home Assistant's certificate did not validate. If it is self-signed "
|
|
941
|
+
"or issued for a different hostname than the configured HA URL, set "
|
|
942
|
+
"HA_VERIFY_SSL=false to skip verification, or reach HA at the "
|
|
943
|
+
"hostname the certificate was issued for.",
|
|
944
|
+
]
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _build_addon_http_client(
|
|
948
|
+
client: HomeAssistantClient, timeout: int, slug: str
|
|
949
|
+
) -> httpx.AsyncClient:
|
|
950
|
+
"""Build the proxy's HTTP client, honoring the HA verify setting.
|
|
951
|
+
|
|
952
|
+
httpx builds its TLS context at construction; doing it before the request
|
|
953
|
+
try reports a CA-store failure as such rather than letting it escape
|
|
954
|
+
unstructured.
|
|
955
|
+
"""
|
|
956
|
+
try:
|
|
957
|
+
return httpx.AsyncClient(
|
|
958
|
+
timeout=httpx.Timeout(timeout),
|
|
959
|
+
verify=client.verify_ssl,
|
|
960
|
+
)
|
|
961
|
+
except OSError as e:
|
|
962
|
+
_tls_setup_error(e, slug)
|
|
963
|
+
return None # py/mixed-returns: unreachable, _tls_setup_error raises
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _raise_connect_failure(
|
|
967
|
+
client: HomeAssistantClient,
|
|
968
|
+
e: Exception,
|
|
969
|
+
*,
|
|
970
|
+
label: str,
|
|
971
|
+
url: str,
|
|
972
|
+
slug: str,
|
|
973
|
+
port: int | None,
|
|
974
|
+
) -> NoReturn:
|
|
975
|
+
"""Raise the structured connect-phase error, classifying TLS failures.
|
|
976
|
+
|
|
977
|
+
A certificate-verification failure with verification enabled gets the TLS
|
|
978
|
+
remedy; everything else keeps the route-appropriate network guidance.
|
|
979
|
+
"""
|
|
980
|
+
if _is_ssl_error(e) and client.verify_ssl:
|
|
981
|
+
raise_tool_error(
|
|
982
|
+
create_error_response(
|
|
983
|
+
ErrorCode.CONNECTION_FAILED,
|
|
984
|
+
f"TLS verification failed connecting to {label}: {e!s}",
|
|
985
|
+
details=f"url={url}",
|
|
986
|
+
context={
|
|
987
|
+
"slug": slug,
|
|
988
|
+
"direct_port": bool(port),
|
|
989
|
+
"verify_ssl": True,
|
|
990
|
+
},
|
|
991
|
+
suggestions=_tls_verification_failure_suggestions(),
|
|
992
|
+
)
|
|
993
|
+
)
|
|
994
|
+
raise_tool_error(
|
|
995
|
+
create_error_response(
|
|
996
|
+
ErrorCode.CONNECTION_FAILED,
|
|
997
|
+
f"Failed to connect to {label}: {e!s}",
|
|
998
|
+
details=f"url={url}",
|
|
999
|
+
context={"slug": slug, "direct_port": bool(port)},
|
|
1000
|
+
suggestions=_addon_connection_failure_suggestions(client, port),
|
|
1001
|
+
)
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
|
|
895
1005
|
async def _run_ws_session(
|
|
896
1006
|
ws_url: str,
|
|
897
1007
|
headers: dict[str, str],
|
|
@@ -900,6 +1010,7 @@ async def _run_ws_session(
|
|
|
900
1010
|
timeout: int,
|
|
901
1011
|
wait_for_close: bool,
|
|
902
1012
|
caller_capped: bool,
|
|
1013
|
+
ssl_context: ssl.SSLContext | None,
|
|
903
1014
|
) -> tuple[list[str], int, str, float]:
|
|
904
1015
|
"""Connect to a WebSocket URL, optionally send body, collect messages.
|
|
905
1016
|
|
|
@@ -916,6 +1027,7 @@ async def _run_ws_session(
|
|
|
916
1027
|
max_size=5 * 1024 * 1024, # 5MB max per message
|
|
917
1028
|
open_timeout=10,
|
|
918
1029
|
close_timeout=5,
|
|
1030
|
+
ssl=ssl_context,
|
|
919
1031
|
) as ws:
|
|
920
1032
|
if body is not None:
|
|
921
1033
|
await ws.send(json.dumps(body) if isinstance(body, dict) else str(body))
|
|
@@ -1024,6 +1136,58 @@ def _build_ws_result(
|
|
|
1024
1136
|
return result
|
|
1025
1137
|
|
|
1026
1138
|
|
|
1139
|
+
def _front_door_state(addon: dict[str, Any]) -> bool | None:
|
|
1140
|
+
"""Return the app's ``leave_front_door_open`` state, or None when unexposed.
|
|
1141
|
+
|
|
1142
|
+
Supervisor's ``options`` dict omits an option the user never saved even
|
|
1143
|
+
when the app's schema exposes it, and the app treats that absence as
|
|
1144
|
+
disabled — so the schema, not key presence, decides whether the option
|
|
1145
|
+
applies (stock installs ship the key in the schema only).
|
|
1146
|
+
"""
|
|
1147
|
+
options = addon.get("options")
|
|
1148
|
+
if isinstance(options, dict) and "leave_front_door_open" in options:
|
|
1149
|
+
return bool(options["leave_front_door_open"])
|
|
1150
|
+
schema = addon.get("schema")
|
|
1151
|
+
if isinstance(schema, list) and any(
|
|
1152
|
+
isinstance(item, dict) and item.get("name") == "leave_front_door_open"
|
|
1153
|
+
for item in schema
|
|
1154
|
+
):
|
|
1155
|
+
return False
|
|
1156
|
+
return None
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def _ws_auth_error_suggestions(
|
|
1160
|
+
addon: dict[str, Any], slug: str, port: int | None, status: int
|
|
1161
|
+
) -> list[str]:
|
|
1162
|
+
"""Return route-appropriate guidance for a rejected WS handshake."""
|
|
1163
|
+
if port:
|
|
1164
|
+
# Prefer the caller-resolved slug; fall back to the addon dict, then a
|
|
1165
|
+
# placeholder, so the suggestion never renders slug=''.
|
|
1166
|
+
slug_val = slug or addon.get("slug") or "<slug>"
|
|
1167
|
+
if _front_door_state(addon) is False:
|
|
1168
|
+
primary = _direct_port_auth_suggestion(slug_val)
|
|
1169
|
+
else:
|
|
1170
|
+
primary = _direct_port_rejection_suggestion()
|
|
1171
|
+
elif is_running_in_addon():
|
|
1172
|
+
# The addon-variant route authenticates with Supervisor ingress
|
|
1173
|
+
# headers, not an ingress session or HA token — do not send the
|
|
1174
|
+
# caller to inspect credentials this request never carried.
|
|
1175
|
+
primary = (
|
|
1176
|
+
"The app (add-on) rejected the ingress-port handshake. This route "
|
|
1177
|
+
"authenticates with Supervisor ingress headers; check that the "
|
|
1178
|
+
"target app is running and its ingress is healthy."
|
|
1179
|
+
)
|
|
1180
|
+
else:
|
|
1181
|
+
primary = (
|
|
1182
|
+
"The ingress session may have expired or your HA token may lack the "
|
|
1183
|
+
"required scope. Verify the token has admin rights and try again."
|
|
1184
|
+
)
|
|
1185
|
+
return [
|
|
1186
|
+
primary,
|
|
1187
|
+
f"Status {status} from the WebSocket handshake.",
|
|
1188
|
+
]
|
|
1189
|
+
|
|
1190
|
+
|
|
1027
1191
|
async def _call_addon_ws(
|
|
1028
1192
|
client: HomeAssistantClient,
|
|
1029
1193
|
slug: str,
|
|
@@ -1115,6 +1279,9 @@ async def _call_addon_ws(
|
|
|
1115
1279
|
requested = max(0, message_offset) + max(0, message_limit)
|
|
1116
1280
|
collection_cap = min(_MAX_WS_MESSAGES, requested)
|
|
1117
1281
|
|
|
1282
|
+
# Built before the try so a CA-store failure is reported as such rather
|
|
1283
|
+
# than as the add-on being unreachable.
|
|
1284
|
+
ssl_context = _build_ws_ssl_context(ws_url, client.verify_ssl, slug)
|
|
1118
1285
|
try:
|
|
1119
1286
|
collected, total_size, close_reason, elapsed = await _run_ws_session(
|
|
1120
1287
|
ws_url,
|
|
@@ -1124,6 +1291,7 @@ async def _call_addon_ws(
|
|
|
1124
1291
|
timeout,
|
|
1125
1292
|
wait_for_close,
|
|
1126
1293
|
caller_capped=message_limit is not None,
|
|
1294
|
+
ssl_context=ssl_context,
|
|
1127
1295
|
)
|
|
1128
1296
|
except websockets.exceptions.InvalidHandshake as e:
|
|
1129
1297
|
suggestions = [
|
|
@@ -1134,12 +1302,7 @@ async def _call_addon_ws(
|
|
|
1134
1302
|
if isinstance(e, websockets.exceptions.InvalidStatus):
|
|
1135
1303
|
status = e.response.status_code
|
|
1136
1304
|
if status in (401, 403):
|
|
1137
|
-
suggestions =
|
|
1138
|
-
"The ingress session may have expired or your HA token "
|
|
1139
|
-
"may lack the required scope. Verify the token has admin "
|
|
1140
|
-
"rights and try again.",
|
|
1141
|
-
f"Status {status} from the WebSocket handshake.",
|
|
1142
|
-
]
|
|
1305
|
+
suggestions = _ws_auth_error_suggestions(addon, slug, port, status)
|
|
1143
1306
|
raise_tool_error(
|
|
1144
1307
|
create_error_response(
|
|
1145
1308
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
@@ -1177,14 +1340,13 @@ async def _call_addon_ws(
|
|
|
1177
1340
|
)
|
|
1178
1341
|
)
|
|
1179
1342
|
except OSError as e:
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
)
|
|
1343
|
+
_raise_connect_failure(
|
|
1344
|
+
client,
|
|
1345
|
+
e,
|
|
1346
|
+
label=f"app (add-on) '{addon_name}' WebSocket",
|
|
1347
|
+
url=ws_url,
|
|
1348
|
+
slug=slug,
|
|
1349
|
+
port=port,
|
|
1188
1350
|
)
|
|
1189
1351
|
|
|
1190
1352
|
return _build_ws_result(
|
|
@@ -1553,57 +1715,106 @@ def _build_http_result(
|
|
|
1553
1715
|
return result
|
|
1554
1716
|
|
|
1555
1717
|
|
|
1718
|
+
def _addon_config_for_http_hint(addon: dict[str, Any]) -> dict[str, Any]:
|
|
1719
|
+
"""Return the app settings that can explain direct-access failures."""
|
|
1720
|
+
return {
|
|
1721
|
+
"options": addon.get("options"),
|
|
1722
|
+
"ports": addon.get("network") or addon.get("ports") or None,
|
|
1723
|
+
"host_network": addon.get("host_network"),
|
|
1724
|
+
"ingress_port": addon.get("ingress_port"),
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
def _direct_port_auth_suggestion(slug: str) -> str:
|
|
1729
|
+
"""Explain the configurable direct-access auth trade-off, Ingress first."""
|
|
1730
|
+
return (
|
|
1731
|
+
"The app (add-on) rejected this direct-port request. Prefer Ingress: retry "
|
|
1732
|
+
f"without the 'port' parameter (ha_manage_app(slug='{slug}', "
|
|
1733
|
+
"path='...')). The app's 'leave_front_door_open' option controls "
|
|
1734
|
+
"direct-access authentication; only if the user explicitly accepts "
|
|
1735
|
+
"the security trade-off, use "
|
|
1736
|
+
f"ha_manage_app(slug='{slug}', "
|
|
1737
|
+
"options={'leave_front_door_open': True}), then "
|
|
1738
|
+
f"ha_manage_app(slug='{slug}', action='restart'), and retry. Enabling "
|
|
1739
|
+
"it removes authentication from the app's direct-access surface for "
|
|
1740
|
+
"hosts that can reach the mapped port."
|
|
1741
|
+
)
|
|
1742
|
+
|
|
1743
|
+
|
|
1744
|
+
def _direct_port_rejection_suggestion() -> str:
|
|
1745
|
+
"""Explain a direct-port rejection the front-door option cannot fix.
|
|
1746
|
+
|
|
1747
|
+
Fires when the app exposes no ``leave_front_door_open`` option, or when it
|
|
1748
|
+
is already enabled — either way the remedy lives in the app's own auth
|
|
1749
|
+
configuration, not in that option.
|
|
1750
|
+
"""
|
|
1751
|
+
return (
|
|
1752
|
+
"The app (add-on) rejected this direct-port request. Check the app's own "
|
|
1753
|
+
"authentication and access-control settings, IP allowlist, and logs."
|
|
1754
|
+
)
|
|
1755
|
+
|
|
1756
|
+
|
|
1556
1757
|
def _add_http_error_hints(
|
|
1557
1758
|
result: dict[str, Any],
|
|
1558
1759
|
response: httpx.Response,
|
|
1559
1760
|
addon: dict[str, Any],
|
|
1560
1761
|
slug: str,
|
|
1762
|
+
direct_port: bool,
|
|
1561
1763
|
) -> None:
|
|
1562
1764
|
"""Mutate result to add an error key for 4xx/5xx responses, with tailored suggestions for 401 and 403."""
|
|
1563
1765
|
if response.status_code >= 400:
|
|
1564
1766
|
result["error"] = f"Add-on API returned HTTP {response.status_code}"
|
|
1767
|
+
# Prefer the caller-resolved slug (authoritative); fall back to the
|
|
1768
|
+
# addon dict, then a placeholder only if neither is populated.
|
|
1769
|
+
slug_val = slug or addon.get("slug") or "<slug>"
|
|
1565
1770
|
if response.status_code == 401:
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
"token has admin rights and try again."
|
|
1573
|
-
)
|
|
1574
|
-
elif response.status_code == 403:
|
|
1575
|
-
# 403 is typically an Nginx IP ACL blocking direct access — a
|
|
1576
|
-
# network configuration problem. Attach addon_config so the LLM
|
|
1577
|
-
# can see the port mapping and suggest the correct port override.
|
|
1578
|
-
ports_dict = addon.get("network") or addon.get("ports") or {}
|
|
1579
|
-
unmapped = sorted(k for k, v in ports_dict.items() if v is None)
|
|
1580
|
-
result["addon_config"] = {
|
|
1581
|
-
"options": addon.get("options"),
|
|
1582
|
-
"ports": ports_dict or None,
|
|
1583
|
-
"host_network": addon.get("host_network"),
|
|
1584
|
-
"ingress_port": addon.get("ingress_port"),
|
|
1585
|
-
}
|
|
1586
|
-
# Prefer the caller-resolved slug (authoritative); fall back to the
|
|
1587
|
-
# addon dict, then a placeholder only if neither is populated.
|
|
1588
|
-
slug_val = slug or addon.get("slug") or "<slug>"
|
|
1589
|
-
example_proto = unmapped[0] if unmapped else ""
|
|
1590
|
-
example_port = example_proto.split("/", 1)[0] if example_proto else ""
|
|
1591
|
-
if unmapped and example_port.isdigit():
|
|
1592
|
-
addon_label = addon.get("name") or slug_val
|
|
1593
|
-
result["suggestion"] = (
|
|
1594
|
-
f"Map {example_proto} to a host port in the HA UI "
|
|
1595
|
-
f"('{addon_label}' → Configuration → Network), restart the "
|
|
1596
|
-
f"add-on, then retry with ha_manage_app(slug='{slug_val}', "
|
|
1597
|
-
f"path='...', port={example_port})."
|
|
1598
|
-
)
|
|
1771
|
+
if direct_port:
|
|
1772
|
+
result["addon_config"] = _addon_config_for_http_hint(addon)
|
|
1773
|
+
if _front_door_state(addon) is False:
|
|
1774
|
+
result["suggestion"] = _direct_port_auth_suggestion(slug_val)
|
|
1775
|
+
else:
|
|
1776
|
+
result["suggestion"] = _direct_port_rejection_suggestion()
|
|
1599
1777
|
else:
|
|
1778
|
+
# An ingress 401 is a credential/session problem. Keep network
|
|
1779
|
+
# configuration out of that result so the caller fixes the HA
|
|
1780
|
+
# token or ingress session instead of weakening app authentication.
|
|
1600
1781
|
result["suggestion"] = (
|
|
1601
|
-
"
|
|
1602
|
-
"
|
|
1603
|
-
"
|
|
1604
|
-
"Example: ha_manage_app(slug='...', path='...', port=<direct_port>). "
|
|
1605
|
-
"The user may need to change add-on settings in the HA UI and restart the add-on."
|
|
1782
|
+
"Authentication failed. The ingress session may have expired, "
|
|
1783
|
+
"or your HA token may lack the required scope. Verify the "
|
|
1784
|
+
"token has admin rights and try again."
|
|
1606
1785
|
)
|
|
1786
|
+
elif response.status_code == 403:
|
|
1787
|
+
# A direct-port 403 is app-level access control; an ingress 403 is
|
|
1788
|
+
# typically an Nginx IP ACL blocking direct access — a network
|
|
1789
|
+
# configuration problem. Attach addon_config either way so the LLM
|
|
1790
|
+
# can see the port mapping.
|
|
1791
|
+
result["addon_config"] = _addon_config_for_http_hint(addon)
|
|
1792
|
+
if direct_port:
|
|
1793
|
+
if _front_door_state(addon) is False:
|
|
1794
|
+
result["suggestion"] = _direct_port_auth_suggestion(slug_val)
|
|
1795
|
+
else:
|
|
1796
|
+
result["suggestion"] = _direct_port_rejection_suggestion()
|
|
1797
|
+
else:
|
|
1798
|
+
ports_dict = addon.get("network") or addon.get("ports") or {}
|
|
1799
|
+
unmapped = sorted(k for k, v in ports_dict.items() if v is None)
|
|
1800
|
+
example_proto = unmapped[0] if unmapped else ""
|
|
1801
|
+
example_port = example_proto.split("/", 1)[0] if example_proto else ""
|
|
1802
|
+
if unmapped and example_port.isdigit():
|
|
1803
|
+
addon_label = addon.get("name") or slug_val
|
|
1804
|
+
result["suggestion"] = (
|
|
1805
|
+
f"Map {example_proto} to a host port in the HA UI "
|
|
1806
|
+
f"('{addon_label}' → Configuration → Network), restart the "
|
|
1807
|
+
f"add-on, then retry with ha_manage_app(slug='{slug_val}', "
|
|
1808
|
+
f"path='...', port={example_port})."
|
|
1809
|
+
)
|
|
1810
|
+
else:
|
|
1811
|
+
result["suggestion"] = (
|
|
1812
|
+
"This add-on is blocking direct connections (likely Nginx IP restriction). "
|
|
1813
|
+
"Try using the 'port' parameter to connect to the add-on's direct access port "
|
|
1814
|
+
"(see addon_config.ports above) with 'leave_front_door_open' enabled. "
|
|
1815
|
+
"Example: ha_manage_app(slug='...', path='...', port=<direct_port>). "
|
|
1816
|
+
"The user may need to change add-on settings in the HA UI and restart the add-on."
|
|
1817
|
+
)
|
|
1607
1818
|
|
|
1608
1819
|
|
|
1609
1820
|
async def _call_addon_api(
|
|
@@ -1713,8 +1924,9 @@ async def _call_addon_api(
|
|
|
1713
1924
|
else:
|
|
1714
1925
|
request_content = None
|
|
1715
1926
|
|
|
1927
|
+
addon_http_client = _build_addon_http_client(client, timeout, slug)
|
|
1716
1928
|
try:
|
|
1717
|
-
async with
|
|
1929
|
+
async with addon_http_client as http_client:
|
|
1718
1930
|
response = await http_client.request(
|
|
1719
1931
|
method=method.upper(),
|
|
1720
1932
|
url=url,
|
|
@@ -1738,14 +1950,13 @@ async def _call_addon_api(
|
|
|
1738
1950
|
)
|
|
1739
1951
|
)
|
|
1740
1952
|
except httpx.ConnectError as e:
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
)
|
|
1953
|
+
_raise_connect_failure(
|
|
1954
|
+
client,
|
|
1955
|
+
e,
|
|
1956
|
+
label=f"app (add-on) '{addon_name}'",
|
|
1957
|
+
url=url,
|
|
1958
|
+
slug=slug,
|
|
1959
|
+
port=port,
|
|
1749
1960
|
)
|
|
1750
1961
|
|
|
1751
1962
|
# 6. Parse response body
|
|
@@ -1786,7 +1997,7 @@ async def _call_addon_api(
|
|
|
1786
1997
|
transformed,
|
|
1787
1998
|
truncated,
|
|
1788
1999
|
)
|
|
1789
|
-
_add_http_error_hints(result, response, addon, slug)
|
|
2000
|
+
_add_http_error_hints(result, response, addon, slug, direct_port=bool(port))
|
|
1790
2001
|
return result
|
|
1791
2002
|
|
|
1792
2003
|
|
|
@@ -2884,7 +3095,10 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
2884
3095
|
int | None,
|
|
2885
3096
|
Field(
|
|
2886
3097
|
description="Proxy mode only. Connect to this port instead of the Ingress port. "
|
|
2887
|
-
"Use ha_get_app(slug='...') to find available ports."
|
|
3098
|
+
"Use ha_get_app(slug='...') to find available ports. Some apps, including "
|
|
3099
|
+
"Node-RED, reject direct access unless their leave_front_door_open option is "
|
|
3100
|
+
"enabled and the app is restarted; related errors include an actionable, "
|
|
3101
|
+
"security-qualified ha_manage_app options command.",
|
|
2888
3102
|
default=None,
|
|
2889
3103
|
),
|
|
2890
3104
|
] = None,
|
|
@@ -3086,7 +3300,12 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
3086
3300
|
proxy by default (works on HAOS, Supervised, and off-host PyPI/uvx
|
|
3087
3301
|
installs). Pass `port=...` to bypass Ingress and connect directly to
|
|
3088
3302
|
an add-on's container port — that mode requires the MCP host to
|
|
3089
|
-
share Home Assistant's container network (
|
|
3303
|
+
share Home Assistant's container network (in practice: the HAOS app
|
|
3304
|
+
(add-on) deployment). Apps such as Node-RED may reject direct requests
|
|
3305
|
+
unless `leave_front_door_open` is enabled in their options and the app
|
|
3306
|
+
is restarted. Authentication errors name the exact
|
|
3307
|
+
`ha_manage_app(options=...)` remedy and its security tradeoff; prefer
|
|
3308
|
+
Ingress when it works.
|
|
3090
3309
|
|
|
3091
3310
|
**ESPHome Device Builder dashboard (current rewrite):** config and log
|
|
3092
3311
|
access is a WebSocket JSON-command API, NOT REST. The legacy endpoints
|
|
@@ -3148,7 +3367,7 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
3148
3367
|
- Change host port: ha_manage_app(slug="...", network={"5800/tcp": 8082})
|
|
3149
3368
|
- Set boot mode: ha_manage_app(slug="...", boot="manual")
|
|
3150
3369
|
- Call HTTP API: ha_manage_app(slug="...", path="/api/events")
|
|
3151
|
-
- Direct port: ha_manage_app(slug="...", path="/flows", port=1880)
|
|
3370
|
+
- Direct port: ha_manage_app(slug="...", path="/flows", port=1880) — if the app rejects it, the error names the 'leave_front_door_open' remedy and its security trade-off; prefer Ingress.
|
|
3152
3371
|
- ESPHome list devices (HTTP): ha_manage_app(slug="<prefix>_esphome", path="/devices")
|
|
3153
3372
|
- ESPHome read a device's YAML (WS one-shot): ha_manage_app(slug="<prefix>_esphome", path="/ws", websocket=True, wait_for_close=False, message_limit=2, body={"command": "devices/get_config", "message_id": "1", "args": {"configuration": "device.yaml"}})
|
|
3154
3373
|
- ESPHome live logs (WS, bounded): ha_manage_app(slug="<prefix>_esphome", path="/ws", websocket=True, wait_for_close=False, message_limit=60, body={"command": "devices/logs", "message_id": "1", "args": {"configuration": "device.yaml", "port": "OTA"}})
|
|
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-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/VENDORED
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/__main__.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
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/client.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/connection.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/frames.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/headers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/http11.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/imports.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/auth.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/http.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/legacy/server.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/protocol.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/proxy.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/py.typed
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/server.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/streams.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/client.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/messages.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/router.py
RENAMED
|
File without changes
|
{ha_mcp_dev-8.3.0.dev2385 → ha_mcp_dev-8.3.0.dev2387}/src/ha_mcp/_vendor/websockets/sync/server.py
RENAMED
|
File without changes
|