ha-mcp-dev 7.12.3.dev1928__tar.gz → 7.12.3.dev1929__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ha_mcp_dev-7.12.3.dev1928/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1929}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/pyproject.toml +1 -1
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/evaluator.py +18 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_code.py +3 -49
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_search.py +4 -1
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_service.py +289 -21
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_system.py +1 -1
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/util_helpers.py +40 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/LICENSE +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/README.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/setup.cfg +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.12.3.
|
|
7
|
+
version = "7.12.3.dev1929"
|
|
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
|
|
@@ -145,4 +145,22 @@ def find_matching_rule(
|
|
|
145
145
|
def evaluate(tool_name: str, args: dict[str, Any], policy: Policy) -> Verdict:
|
|
146
146
|
if find_matching_rule(tool_name, args, policy) is not None:
|
|
147
147
|
return Verdict.REQUIRE_APPROVAL
|
|
148
|
+
# ha_call_service exposes a raw WebSocket escape hatch (``ws_command``) that
|
|
149
|
+
# carries no ``domain``/``service`` argument, so a rule keyed on
|
|
150
|
+
# ``args.domain``/``args.service`` cannot match it and it would otherwise slip
|
|
151
|
+
# through the fail-open default. If the operator has ANY rule that applies to
|
|
152
|
+
# ha_call_service -- one scoped to it by name, or a wildcard ``*`` rule, which
|
|
153
|
+
# ``match_rule`` treats as applying to every tool -- treat an unmatched
|
|
154
|
+
# ws_command call as require-approval (fail safe) so the escape hatch cannot
|
|
155
|
+
# sneak past that oversight. Blocking the call (require-approval) rather than
|
|
156
|
+
# silently allowing it is the safe error direction for a raw WS escape hatch,
|
|
157
|
+
# especially since the write-command blocklist is a deliberately
|
|
158
|
+
# non-exhaustive wrapper-bypass list that leans on this gate.
|
|
159
|
+
# Operators who want finer control can add a rule keyed on ``args.ws_command``.
|
|
160
|
+
if (
|
|
161
|
+
tool_name == "ha_call_service"
|
|
162
|
+
and args.get("ws_command")
|
|
163
|
+
and any(rule.tool_name in ("ha_call_service", "*") for rule in policy.rules)
|
|
164
|
+
):
|
|
165
|
+
return Verdict.REQUIRE_APPROVAL
|
|
148
166
|
return Verdict.ALLOW
|
|
@@ -36,6 +36,7 @@ from fastmcp.server.context import Context
|
|
|
36
36
|
from ..config import get_global_settings
|
|
37
37
|
from ..errors import ErrorCode, create_error_response
|
|
38
38
|
from .helpers import log_tool_usage, raise_tool_error
|
|
39
|
+
from .util_helpers import BLOCKED_WS_WRITE_COMMANDS
|
|
39
40
|
|
|
40
41
|
logger = logging.getLogger(__name__)
|
|
41
42
|
|
|
@@ -156,53 +157,6 @@ _BLOCKED_HA_INTERNAL_EVENTS: frozenset[str] = frozenset(
|
|
|
156
157
|
}
|
|
157
158
|
)
|
|
158
159
|
|
|
159
|
-
# WebSocket commands the sandbox must not send. Each one either changes
|
|
160
|
-
# persistent state in a way that bypasses a wrapping tool's validation
|
|
161
|
-
# (lovelace, registry mutations) or has no sandbox-appropriate use case
|
|
162
|
-
# at all (``config/core/update`` rewrites the HA installation's location/
|
|
163
|
-
# timezone/currency/lat-long).
|
|
164
|
-
_BLOCKED_WS_COMMANDS: frozenset[str] = frozenset(
|
|
165
|
-
{
|
|
166
|
-
"config/core/update",
|
|
167
|
-
"lovelace/config/save",
|
|
168
|
-
"lovelace/dashboards/create",
|
|
169
|
-
"lovelace/dashboards/delete",
|
|
170
|
-
"lovelace/dashboards/update",
|
|
171
|
-
"config/area_registry/delete",
|
|
172
|
-
"config/area_registry/disable",
|
|
173
|
-
"config/area_registry/update",
|
|
174
|
-
"config/device_registry/delete",
|
|
175
|
-
"config/device_registry/disable",
|
|
176
|
-
"config/device_registry/update",
|
|
177
|
-
# Device registry deletion is registered as ``remove_config_entry`` on
|
|
178
|
-
# HA Core, not ``delete`` — see ``tools_registry.py:753`` for the
|
|
179
|
-
# actually-emitted command. ``ha_remove_device`` wraps it; raw
|
|
180
|
-
# ``ws_send`` would skip those checks.
|
|
181
|
-
"config/device_registry/remove_config_entry",
|
|
182
|
-
"config/entity_registry/delete",
|
|
183
|
-
"config/entity_registry/disable",
|
|
184
|
-
"config/entity_registry/update",
|
|
185
|
-
# Entity registry deletion is registered as ``remove`` on HA Core,
|
|
186
|
-
# not ``delete`` — see ``tools_entities.py:1130`` for the
|
|
187
|
-
# actually-emitted command. ``ha_remove_entity`` wraps it.
|
|
188
|
-
"config/entity_registry/remove",
|
|
189
|
-
# Floor / label / category registries follow the same rationale as
|
|
190
|
-
# area / device / entity above: each has a wrapping MCP tool
|
|
191
|
-
# (``ha_set_area_or_floor``, ``ha_config_set_label``,
|
|
192
|
-
# ``ha_config_set_category``) that performs invariant checks the
|
|
193
|
-
# raw WS command skips.
|
|
194
|
-
"config/floor_registry/create",
|
|
195
|
-
"config/floor_registry/delete",
|
|
196
|
-
"config/floor_registry/update",
|
|
197
|
-
"config/label_registry/create",
|
|
198
|
-
"config/label_registry/delete",
|
|
199
|
-
"config/label_registry/update",
|
|
200
|
-
"config/category_registry/create",
|
|
201
|
-
"config/category_registry/delete",
|
|
202
|
-
"config/category_registry/update",
|
|
203
|
-
}
|
|
204
|
-
)
|
|
205
|
-
|
|
206
160
|
|
|
207
161
|
def _classify_sandbox_error(exc: Exception) -> tuple[ErrorCode, str, list[str]]:
|
|
208
162
|
"""Map a sandbox exception to ``(code, message, suggestions)``.
|
|
@@ -859,7 +813,7 @@ class _SandboxBridge:
|
|
|
859
813
|
code is dynamic and may pass non-dict values; the runtime guard
|
|
860
814
|
below converts that into an error dict.
|
|
861
815
|
|
|
862
|
-
Commands listed in ``
|
|
816
|
+
Commands listed in ``BLOCKED_WS_WRITE_COMMANDS`` are rejected with an
|
|
863
817
|
explanatory error: those either rewrite persistent state in ways
|
|
864
818
|
that have no sandbox-appropriate use case (``config/core/update``)
|
|
865
819
|
or bypass the validation in their wrapping MCP tool
|
|
@@ -876,7 +830,7 @@ class _SandboxBridge:
|
|
|
876
830
|
msg_type = message.get("type")
|
|
877
831
|
if not isinstance(msg_type, str):
|
|
878
832
|
return {"error": "ws_send(message) requires a 'type' field"}
|
|
879
|
-
if msg_type in
|
|
833
|
+
if msg_type in BLOCKED_WS_WRITE_COMMANDS:
|
|
880
834
|
logger.info("sandbox.ws_send.blocked type=%r", msg_type)
|
|
881
835
|
return {
|
|
882
836
|
"error": (
|
|
@@ -2689,7 +2689,10 @@ class SearchTools:
|
|
|
2689
2689
|
default=False,
|
|
2690
2690
|
description=(
|
|
2691
2691
|
"Include user-dismissed/ignored repairs (default: False). "
|
|
2692
|
-
"Matches the HA Repairs UI which hides dismissed items by default."
|
|
2692
|
+
"Matches the HA Repairs UI which hides dismissed items by default. "
|
|
2693
|
+
"To dismiss/ignore a repair, call ha_call_service with "
|
|
2694
|
+
'ws_command="repairs/ignore_issue" and data={"domain": ..., '
|
|
2695
|
+
'"issue_id": ..., "ignore": true}.'
|
|
2693
2696
|
),
|
|
2694
2697
|
),
|
|
2695
2698
|
] = False,
|
|
@@ -15,6 +15,8 @@ from pydantic import Field
|
|
|
15
15
|
|
|
16
16
|
from ..client.rest_client import HomeAssistantConnectionError
|
|
17
17
|
from ..errors import (
|
|
18
|
+
ErrorCode,
|
|
19
|
+
create_error_response,
|
|
18
20
|
create_validation_error,
|
|
19
21
|
)
|
|
20
22
|
from .helpers import (
|
|
@@ -24,6 +26,7 @@ from .helpers import (
|
|
|
24
26
|
register_tool_methods,
|
|
25
27
|
)
|
|
26
28
|
from .util_helpers import (
|
|
29
|
+
BLOCKED_WS_WRITE_COMMANDS,
|
|
27
30
|
JSON_STRING_COERCION,
|
|
28
31
|
compact_service_result,
|
|
29
32
|
parse_json_param,
|
|
@@ -126,6 +129,50 @@ _SERVICE_TO_STATE: dict[str, str] = {
|
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
|
|
132
|
+
# WebSocket commands that stream events or reply in two phases (an initial ack
|
|
133
|
+
# then the real payload as a follow-up event) rather than resolving to a single
|
|
134
|
+
# terminal result. ha_call_service's ws_command escape hatch only supports
|
|
135
|
+
# one-shot request/response commands, so these are rejected: the "subscribe"
|
|
136
|
+
# substring catches subscription commands, and the set names known two-phase /
|
|
137
|
+
# streaming commands whose names don't contain "subscribe". The set is a floor,
|
|
138
|
+
# not an exhaustive list -- HA's WS command naming isn't consistent enough for a
|
|
139
|
+
# name check alone to be fully reliable.
|
|
140
|
+
_WS_COMMAND_EVENT_BLOCKLIST = frozenset(
|
|
141
|
+
{
|
|
142
|
+
"render_template", # event-based; use ha_eval_template instead
|
|
143
|
+
"system_health/info", # two-phase (see tools_system._fetch_health_info)
|
|
144
|
+
"assist_pipeline/run", # streams pipeline events
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Substrings that mark a WS command as streaming / subscription-based even when
|
|
149
|
+
# its name isn't in the blocklist above. Such commands ack once and then push
|
|
150
|
+
# follow-up events on the same id; the one-shot send_command path returns the
|
|
151
|
+
# ack and leaks the subscription. "subscribe" covers subscribe_* and */subscribe;
|
|
152
|
+
# "stream" covers history/stream, logbook/event_stream, camera/stream, ...;
|
|
153
|
+
# "start_preview" covers template/start_preview and the config-flow preview family.
|
|
154
|
+
_WS_STREAMING_SUBSTRINGS = ("subscribe", "stream", "start_preview")
|
|
155
|
+
|
|
156
|
+
# One-shot WS commands that re-enter Home Assistant's service invocation. Routing
|
|
157
|
+
# them through the escape hatch would bypass the service-mode guards (notably the
|
|
158
|
+
# reserved ha_mcp_tools domain block), so they are rejected -- use the
|
|
159
|
+
# domain/service parameters for service calls instead.
|
|
160
|
+
_WS_COMMAND_SERVICE_INVOKERS = frozenset({"call_service", "execute_script"})
|
|
161
|
+
|
|
162
|
+
# Reserved WebSocket envelope keys the transport owns. Allowing them inside data
|
|
163
|
+
# would let a caller override the validated command type (defeating every check
|
|
164
|
+
# below) or collide with the transport's message id, so they are rejected.
|
|
165
|
+
_WS_RESERVED_ENVELOPE_KEYS = frozenset({"type", "id"})
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _is_streaming_ws_command(command_type: str) -> bool:
|
|
169
|
+
"""Return True for subscription / streaming / two-phase WS commands."""
|
|
170
|
+
lowered = command_type.lower()
|
|
171
|
+
if lowered in _WS_COMMAND_EVENT_BLOCKLIST:
|
|
172
|
+
return True
|
|
173
|
+
return any(sub in lowered for sub in _WS_STREAMING_SUBSTRINGS)
|
|
174
|
+
|
|
175
|
+
|
|
129
176
|
def _build_service_suggestions(
|
|
130
177
|
domain: str, service: str, entity_id: str | None
|
|
131
178
|
) -> list[str]:
|
|
@@ -162,6 +209,80 @@ class ServiceTools:
|
|
|
162
209
|
service_data["entity_id"] = entity_id
|
|
163
210
|
return service_data
|
|
164
211
|
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _validate_service_call_params(
|
|
214
|
+
domain: str | None, service: str | None
|
|
215
|
+
) -> tuple[str, str]:
|
|
216
|
+
"""Validate service-mode params and return the (domain, service) pair.
|
|
217
|
+
|
|
218
|
+
Raises a structured ToolError when domain/service are missing (the caller
|
|
219
|
+
likely wants the ws_command escape hatch) or when the domain targets the
|
|
220
|
+
reserved ha_mcp_tools namespace.
|
|
221
|
+
"""
|
|
222
|
+
if not domain or not service:
|
|
223
|
+
raise_tool_error(
|
|
224
|
+
create_validation_error(
|
|
225
|
+
"domain and service are required for a service call. To send "
|
|
226
|
+
"a raw WebSocket command instead, pass ws_command.",
|
|
227
|
+
parameter="domain" if not domain else "service",
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
# ha_mcp_tools.* services are restricted to the ha-mcp server's dedicated
|
|
231
|
+
# wrappers (which inject the required caller token). Block ha_call_service
|
|
232
|
+
# from forwarding to that domain — it would otherwise be a bypass path
|
|
233
|
+
# around the dedicated tools. HA core's service registry lowercases the
|
|
234
|
+
# domain on fallback lookup (homeassistant/core.py
|
|
235
|
+
# ServiceRegistry.async_call), so normalise here to make sure a mixed-case
|
|
236
|
+
# `HA_MCP_TOOLS` can't slip past this exact-string check and still resolve
|
|
237
|
+
# downstream.
|
|
238
|
+
if domain.strip().lower() == "ha_mcp_tools":
|
|
239
|
+
raise_tool_error(
|
|
240
|
+
create_validation_error(
|
|
241
|
+
(
|
|
242
|
+
"ha_call_service cannot invoke services in the "
|
|
243
|
+
"'ha_mcp_tools' domain. Use the dedicated MCP tool "
|
|
244
|
+
"instead: ha_list_files, ha_read_file, ha_write_file, "
|
|
245
|
+
"ha_delete_file, or ha_config_set_yaml."
|
|
246
|
+
),
|
|
247
|
+
parameter="domain",
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
return domain, service
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def _reject_incompatible_ws_params(
|
|
254
|
+
entity_id: str | None,
|
|
255
|
+
return_response: bool,
|
|
256
|
+
verbose: bool,
|
|
257
|
+
result_fields: str | list[str] | None,
|
|
258
|
+
result_attribute_keys: str | list[str] | None,
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Reject service-mode-only params when the ws_command escape hatch is used.
|
|
261
|
+
|
|
262
|
+
These shape a registered-service call and have no meaning for a raw
|
|
263
|
+
WebSocket command; silently ignoring them would be a confusing no-op, so
|
|
264
|
+
(mirroring the domain/service "not both" guard) they must be omitted.
|
|
265
|
+
"""
|
|
266
|
+
offenders = [
|
|
267
|
+
name
|
|
268
|
+
for name, is_set in (
|
|
269
|
+
("entity_id", entity_id is not None),
|
|
270
|
+
("return_response", return_response),
|
|
271
|
+
("verbose", verbose),
|
|
272
|
+
("result_fields", result_fields is not None),
|
|
273
|
+
("result_attribute_keys", result_attribute_keys is not None),
|
|
274
|
+
)
|
|
275
|
+
if is_set
|
|
276
|
+
]
|
|
277
|
+
if offenders:
|
|
278
|
+
raise_tool_error(
|
|
279
|
+
create_validation_error(
|
|
280
|
+
"These parameters apply only to service calls and must be "
|
|
281
|
+
f"omitted when ws_command is set: {', '.join(offenders)}.",
|
|
282
|
+
parameter="ws_command",
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
|
|
165
286
|
@staticmethod
|
|
166
287
|
def _parse_result_projection_params(
|
|
167
288
|
result_fields: str | list[str] | None,
|
|
@@ -372,6 +493,125 @@ class ServiceTools:
|
|
|
372
493
|
suggestions=suggestions,
|
|
373
494
|
)
|
|
374
495
|
|
|
496
|
+
async def _call_ws_command(
|
|
497
|
+
self,
|
|
498
|
+
ws_command: str,
|
|
499
|
+
data: str | dict[str, Any] | None,
|
|
500
|
+
*,
|
|
501
|
+
domain: str | None,
|
|
502
|
+
service: str | None,
|
|
503
|
+
) -> dict[str, Any]:
|
|
504
|
+
"""Send a one-shot WebSocket command via ha_call_service's escape hatch.
|
|
505
|
+
|
|
506
|
+
Reaches Home Assistant WebSocket commands that are not registered
|
|
507
|
+
services (e.g. ``repairs/ignore_issue``). Only one-shot
|
|
508
|
+
request/response commands are supported — streaming / subscription
|
|
509
|
+
commands are rejected up front.
|
|
510
|
+
"""
|
|
511
|
+
command_type = ws_command.strip()
|
|
512
|
+
if domain is not None or service is not None:
|
|
513
|
+
raise_tool_error(
|
|
514
|
+
create_validation_error(
|
|
515
|
+
"Provide either domain + service (a registered service call) "
|
|
516
|
+
"OR ws_command (a raw WebSocket command), not both.",
|
|
517
|
+
parameter="ws_command",
|
|
518
|
+
)
|
|
519
|
+
)
|
|
520
|
+
if not command_type:
|
|
521
|
+
raise_tool_error(
|
|
522
|
+
create_validation_error(
|
|
523
|
+
"ws_command must be a non-empty WebSocket command type, "
|
|
524
|
+
"e.g. 'repairs/ignore_issue'.",
|
|
525
|
+
parameter="ws_command",
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
if _is_streaming_ws_command(command_type):
|
|
529
|
+
raise_tool_error(
|
|
530
|
+
create_validation_error(
|
|
531
|
+
f"ws_command '{command_type}' is a streaming or two-phase "
|
|
532
|
+
"command; ha_call_service only sends one-shot "
|
|
533
|
+
"request/response commands. For template rendering use "
|
|
534
|
+
"ha_eval_template.",
|
|
535
|
+
parameter="ws_command",
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
if command_type.lower() in _WS_COMMAND_SERVICE_INVOKERS:
|
|
539
|
+
raise_tool_error(
|
|
540
|
+
create_validation_error(
|
|
541
|
+
f"ws_command '{command_type}' invokes Home Assistant services "
|
|
542
|
+
"and would bypass ha_call_service's safeguards. Use the "
|
|
543
|
+
"domain/service parameters for service calls instead.",
|
|
544
|
+
parameter="ws_command",
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
if command_type.lower().startswith("ha_mcp_tools/"):
|
|
548
|
+
raise_tool_error(
|
|
549
|
+
create_validation_error(
|
|
550
|
+
"ha_call_service cannot invoke 'ha_mcp_tools/*' WebSocket "
|
|
551
|
+
"commands. Use the dedicated ha-mcp tools instead.",
|
|
552
|
+
parameter="ws_command",
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
if command_type.lower() in BLOCKED_WS_WRITE_COMMANDS:
|
|
556
|
+
raise_tool_error(
|
|
557
|
+
create_validation_error(
|
|
558
|
+
f"ws_command '{command_type}' mutates persistent state that a "
|
|
559
|
+
"dedicated tool guards with backups and conflict checks. Use "
|
|
560
|
+
"the corresponding ha-mcp tool (e.g. ha_config_set_dashboard, "
|
|
561
|
+
"ha_set_area_or_floor, ha_remove_entity) instead.",
|
|
562
|
+
parameter="ws_command",
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
command_params = (
|
|
566
|
+
_parse_json_dict_param(
|
|
567
|
+
data, type_error_message="ws_command data must be a JSON object"
|
|
568
|
+
)
|
|
569
|
+
or {}
|
|
570
|
+
)
|
|
571
|
+
reserved = _WS_RESERVED_ENVELOPE_KEYS & command_params.keys()
|
|
572
|
+
if reserved:
|
|
573
|
+
raise_tool_error(
|
|
574
|
+
create_validation_error(
|
|
575
|
+
"data must not contain the reserved WebSocket envelope key(s) "
|
|
576
|
+
f"{', '.join(sorted(reserved))}; the command type is set by "
|
|
577
|
+
"ws_command, and the message id is managed by the transport.",
|
|
578
|
+
parameter="data",
|
|
579
|
+
)
|
|
580
|
+
)
|
|
581
|
+
# send_websocket_message catches its own transport errors and always
|
|
582
|
+
# returns a {"success": ...} dict (never raises), so the failure is
|
|
583
|
+
# handled by the result-shape check below -- matching the other
|
|
584
|
+
# send_websocket_message call sites in the codebase.
|
|
585
|
+
result = await self._client.send_websocket_message(
|
|
586
|
+
{"type": command_type, **command_params}
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
if not isinstance(result, dict) or not result.get("success", False):
|
|
590
|
+
error_msg = (
|
|
591
|
+
result.get("error") if isinstance(result, dict) else None
|
|
592
|
+
) or "WebSocket command failed"
|
|
593
|
+
raise_tool_error(
|
|
594
|
+
create_error_response(
|
|
595
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
596
|
+
str(error_msg),
|
|
597
|
+
context={"ws_command": command_type},
|
|
598
|
+
suggestions=[
|
|
599
|
+
"Verify the command type and its parameters (e.g. "
|
|
600
|
+
+ "repairs/ignore_issue needs domain, issue_id, ignore)",
|
|
601
|
+
"Confirm the target still exists (a repair must be "
|
|
602
|
+
+ "present to ignore it)",
|
|
603
|
+
],
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
"success": True,
|
|
609
|
+
"ws_command": command_type,
|
|
610
|
+
"parameters": command_params or None,
|
|
611
|
+
"result": result.get("result"),
|
|
612
|
+
"message": f"Successfully executed WebSocket command '{command_type}'",
|
|
613
|
+
}
|
|
614
|
+
|
|
375
615
|
@tool(
|
|
376
616
|
name="ha_call_service",
|
|
377
617
|
tags={"Service & Device Control"},
|
|
@@ -380,8 +620,8 @@ class ServiceTools:
|
|
|
380
620
|
@log_tool_usage
|
|
381
621
|
async def ha_call_service(
|
|
382
622
|
self,
|
|
383
|
-
domain: str,
|
|
384
|
-
service: str,
|
|
623
|
+
domain: str | None = None,
|
|
624
|
+
service: str | None = None,
|
|
385
625
|
entity_id: str | None = None,
|
|
386
626
|
data: Annotated[dict[str, Any] | None, JSON_STRING_COERCION] = None,
|
|
387
627
|
return_response: bool = False,
|
|
@@ -425,6 +665,21 @@ class ServiceTools:
|
|
|
425
665
|
),
|
|
426
666
|
),
|
|
427
667
|
] = None,
|
|
668
|
+
ws_command: Annotated[
|
|
669
|
+
str | None,
|
|
670
|
+
Field(
|
|
671
|
+
default=None,
|
|
672
|
+
description=(
|
|
673
|
+
"Advanced escape hatch: send a raw one-shot Home Assistant "
|
|
674
|
+
"WebSocket command that is NOT a registered service (e.g. "
|
|
675
|
+
"'repairs/ignore_issue' to dismiss a Repairs issue). When set, "
|
|
676
|
+
"omit domain/service and the other service params; put the "
|
|
677
|
+
"command's parameters in data. Streaming/two-phase and "
|
|
678
|
+
"service-invoking commands (call_service, execute_script) are "
|
|
679
|
+
"rejected."
|
|
680
|
+
),
|
|
681
|
+
),
|
|
682
|
+
] = None,
|
|
428
683
|
) -> dict[str, Any]:
|
|
429
684
|
"""
|
|
430
685
|
Execute Home Assistant services to control entities and trigger automations.
|
|
@@ -462,27 +717,40 @@ class ServiceTools:
|
|
|
462
717
|
|
|
463
718
|
Common patterns: Use ha_get_state() to check current values before making changes.
|
|
464
719
|
Use ha_search() to find correct entity IDs.
|
|
720
|
+
|
|
721
|
+
**WebSocket command escape hatch (advanced):**
|
|
722
|
+
A few Home Assistant operations are WebSocket-only commands, not
|
|
723
|
+
registered services — most notably dismissing a Repairs issue. Pass
|
|
724
|
+
``ws_command`` (instead of domain/service) to send one, with its
|
|
725
|
+
parameters in ``data``:
|
|
726
|
+
```python
|
|
727
|
+
# Dismiss a repair (get domain/issue_id from ha_get_overview repairs
|
|
728
|
+
# or ha_get_system_health include="repairs")
|
|
729
|
+
ha_call_service(ws_command="repairs/ignore_issue",
|
|
730
|
+
data={"domain": "sun", "issue_id": "abc", "ignore": True})
|
|
731
|
+
```
|
|
732
|
+
Only one-shot request/response commands are supported; streaming/two-phase
|
|
733
|
+
and service-invoking commands are rejected, and the other service
|
|
734
|
+
parameters (entity_id, return_response, etc.) don't apply.
|
|
465
735
|
"""
|
|
466
|
-
#
|
|
467
|
-
#
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
"ha_call_service cannot invoke services in the "
|
|
479
|
-
"'ha_mcp_tools' domain. Use the dedicated MCP tool "
|
|
480
|
-
"instead: ha_list_files, ha_read_file, ha_write_file, "
|
|
481
|
-
"ha_delete_file, or ha_config_set_yaml."
|
|
482
|
-
),
|
|
483
|
-
parameter="domain",
|
|
484
|
-
)
|
|
736
|
+
# WebSocket-command escape hatch (issue #1839): reach one-shot WS
|
|
737
|
+
# commands that aren't registered services (e.g. repairs/ignore_issue).
|
|
738
|
+
if ws_command is not None:
|
|
739
|
+
self._reject_incompatible_ws_params(
|
|
740
|
+
entity_id,
|
|
741
|
+
return_response,
|
|
742
|
+
verbose,
|
|
743
|
+
result_fields,
|
|
744
|
+
result_attribute_keys,
|
|
745
|
+
)
|
|
746
|
+
return await self._call_ws_command(
|
|
747
|
+
ws_command, data, domain=domain, service=service
|
|
485
748
|
)
|
|
749
|
+
|
|
750
|
+
# Service mode requires domain + service (optional at the signature level
|
|
751
|
+
# only to make room for the ws_command escape hatch) and rejects the
|
|
752
|
+
# reserved ha_mcp_tools domain.
|
|
753
|
+
domain, service = self._validate_service_call_params(domain, service)
|
|
486
754
|
try:
|
|
487
755
|
service_data = self._parse_service_data(data, entity_id)
|
|
488
756
|
|
|
@@ -541,7 +541,7 @@ class SystemTools:
|
|
|
541
541
|
|
|
542
542
|
**Parameters:**
|
|
543
543
|
- include: Optional comma-separated list of additional data to include.
|
|
544
|
-
- "repairs": Repair items from Settings > System > Repairs (active only by default; pass `include_dismissed_repairs=True` for all)
|
|
544
|
+
- "repairs": Repair items from Settings > System > Repairs (active only by default; pass `include_dismissed_repairs=True` for all). To dismiss/ignore a repair, call `ha_call_service(ws_command="repairs/ignore_issue", data={"domain": <domain>, "issue_id": <issue_id>, "ignore": true})`.
|
|
545
545
|
- "zha_network": ZHA Zigbee devices with radio signal summary (name, LQI, RSSI)
|
|
546
546
|
- "zha_network_full": ZHA Zigbee devices with all device details (can be large on 100+ device networks; prefer "zha_network" for summary)
|
|
547
547
|
- "zwave_network": Z-Wave JS network status and node summary (status, security, routing)
|
|
@@ -480,6 +480,46 @@ def unwrap_service_response(result: dict[str, Any]) -> dict[str, Any]:
|
|
|
480
480
|
return sr if isinstance(sr, dict) else result
|
|
481
481
|
|
|
482
482
|
|
|
483
|
+
# WebSocket commands that mutate persistent state in a way that bypasses a
|
|
484
|
+
# wrapping MCP tool's validation (auto-backup, config-hash optimistic locking,
|
|
485
|
+
# registry invariant checks), or that have no escape-hatch-appropriate use case
|
|
486
|
+
# (`config/core/update` rewrites the installation's location/timezone/currency/
|
|
487
|
+
# lat-long). Shared by the code sandbox's `ws_send` bridge (tools_code.py) and
|
|
488
|
+
# ha_call_service's `ws_command` escape hatch (tools_service.py) so the two raw
|
|
489
|
+
# WebSocket surfaces stay in lockstep. Registry deletion command names differ on
|
|
490
|
+
# HA Core: device deletion is `remove_config_entry` and entity deletion is
|
|
491
|
+
# `remove` (not `delete`) -- ha_remove_device / ha_remove_entity emit those.
|
|
492
|
+
BLOCKED_WS_WRITE_COMMANDS: frozenset[str] = frozenset(
|
|
493
|
+
{
|
|
494
|
+
"config/core/update",
|
|
495
|
+
"lovelace/config/save",
|
|
496
|
+
"lovelace/dashboards/create",
|
|
497
|
+
"lovelace/dashboards/delete",
|
|
498
|
+
"lovelace/dashboards/update",
|
|
499
|
+
"config/area_registry/delete",
|
|
500
|
+
"config/area_registry/disable",
|
|
501
|
+
"config/area_registry/update",
|
|
502
|
+
"config/device_registry/delete",
|
|
503
|
+
"config/device_registry/disable",
|
|
504
|
+
"config/device_registry/update",
|
|
505
|
+
"config/device_registry/remove_config_entry",
|
|
506
|
+
"config/entity_registry/delete",
|
|
507
|
+
"config/entity_registry/disable",
|
|
508
|
+
"config/entity_registry/update",
|
|
509
|
+
"config/entity_registry/remove",
|
|
510
|
+
"config/floor_registry/create",
|
|
511
|
+
"config/floor_registry/delete",
|
|
512
|
+
"config/floor_registry/update",
|
|
513
|
+
"config/label_registry/create",
|
|
514
|
+
"config/label_registry/delete",
|
|
515
|
+
"config/label_registry/update",
|
|
516
|
+
"config/category_registry/create",
|
|
517
|
+
"config/category_registry/delete",
|
|
518
|
+
"config/category_registry/update",
|
|
519
|
+
}
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
483
523
|
# Fields surfaced from each repair issue. Includes `ignored` / `dismissed_version`
|
|
484
524
|
# so callers can distinguish active vs. user-dismissed repairs when both are
|
|
485
525
|
# returned (e.g., `include_dismissed_repairs=True`).
|
|
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
|
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/supervisor_client.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/websocket_client.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/client/websocket_listener.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/capture.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/content.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/src/ha_mcp/dashboard_screenshot/paths.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1928 → ha_mcp_dev-7.12.3.dev1929}/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
|