ha-mcp-dev 7.12.3.dev1863__tar.gz → 7.12.3.dev1867__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.dev1863/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1867}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/pyproject.toml +8 -1
- ha_mcp_dev-7.12.3.dev1867/src/ha_mcp/llm_exposure.py +214 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/server.py +8 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/settings_ui/__init__.py +67 -2
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/settings_ui/settings.js +42 -3
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/config_entry_flow.py +51 -13
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/LICENSE +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/README.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/setup.cfg +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/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.dev1867"
|
|
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
|
|
@@ -103,6 +103,9 @@ module = [
|
|
|
103
103
|
"awesomeversion",
|
|
104
104
|
"yarl",
|
|
105
105
|
"voluptuous",
|
|
106
|
+
# Ships with HA core at runtime (helpers.llm schema conversion, #1745);
|
|
107
|
+
# not installed in this dev environment.
|
|
108
|
+
"voluptuous_openapi",
|
|
106
109
|
"jsonschema",
|
|
107
110
|
"requests",
|
|
108
111
|
"requests.*",
|
|
@@ -274,6 +277,10 @@ dev = [
|
|
|
274
277
|
# Real voluptuous for the config-flow tests (the component's
|
|
275
278
|
# config_flow imports it; HA provides it at runtime, tests need it here).
|
|
276
279
|
"voluptuous>=0.15.2",
|
|
280
|
+
# Real voluptuous-openapi for the embedded e2e LLM-API test: it converts
|
|
281
|
+
# the REAL tool-schema catalog exactly like llm_api.py does at runtime
|
|
282
|
+
# (HA core provides it at runtime; unit tests stub it).
|
|
283
|
+
"voluptuous-openapi>=0.4.1",
|
|
277
284
|
"pytest-xdist>=3.8.0",
|
|
278
285
|
"requests>=2.25.0",
|
|
279
286
|
"lefthook>=1.10.0",
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Per-tool exposure control for the HA conversation-agent LLM API (#1745).
|
|
2
|
+
|
|
3
|
+
The ``ha_mcp_tools`` custom component registers this server's toolset as a
|
|
4
|
+
Home Assistant LLM API so conversation agents (and through them Assist chat
|
|
5
|
+
and voice) can drive the tools. Which tools that surface offers is a separate
|
|
6
|
+
axis from the global enable/disable in the settings UI:
|
|
7
|
+
|
|
8
|
+
* a tool **disabled** in the settings UI is gone for every client — FastMCP
|
|
9
|
+
removes it from ``tools/list`` and rejects ``tools/call`` by name;
|
|
10
|
+
* a tool **hidden from the LLM API** (this module) stays fully available to
|
|
11
|
+
regular MCP clients (claude.ai, Claude Code, ...) and is only invisible to
|
|
12
|
+
conversation agents — in both the full-catalog and tool-search exposure
|
|
13
|
+
modes of the component.
|
|
14
|
+
|
|
15
|
+
The single source of truth travels **in-band**: :class:`LlmExposureMiddleware`
|
|
16
|
+
stamps every ``tools/list`` entry with
|
|
17
|
+
``_meta.ha_mcp = {"llm_api_exposed": bool, "pinned": bool}`` so the component
|
|
18
|
+
(one more loopback MCP client) filters on data that can never drift from the
|
|
19
|
+
server's settings, with zero extra round-trips. Stamping re-reads the
|
|
20
|
+
persisted settings behind a short coalescing cache (2s TTL), so settings-UI
|
|
21
|
+
changes apply on the agent's next conversation turn without a restart.
|
|
22
|
+
|
|
23
|
+
Defaults are deny-by-default for the risky sets (owner decision, #1745):
|
|
24
|
+
beta-tagged tools, developer-mode tools, and the restart/reload/backup
|
|
25
|
+
family start hidden from conversation agents until a user explicitly enables
|
|
26
|
+
them in the settings UI. Everything else starts exposed — selecting the LLM
|
|
27
|
+
API on an agent is itself an explicit opt-in.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
import time
|
|
34
|
+
from collections.abc import Mapping, Sequence
|
|
35
|
+
from typing import TYPE_CHECKING, Any
|
|
36
|
+
|
|
37
|
+
from fastmcp.server.middleware import Middleware
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from fastmcp.server.middleware import CallNext, MiddlewareContext
|
|
41
|
+
from fastmcp.tools import Tool
|
|
42
|
+
from mcp import types as mt
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
# Key under tools/list ``_meta`` (and the key inside it) the custom component
|
|
47
|
+
# filters on. The component treats an ABSENT stamp as "server too old for
|
|
48
|
+
# per-tool exposure" and falls back to its own conservative deny-list.
|
|
49
|
+
META_NAMESPACE = "ha_mcp"
|
|
50
|
+
META_EXPOSED_KEY = "llm_api_exposed"
|
|
51
|
+
META_PINNED_KEY = "pinned"
|
|
52
|
+
|
|
53
|
+
# Key in tool_config.json holding the user's per-tool overrides
|
|
54
|
+
# ({tool_name: bool}). Sparse on purpose: only tools the user explicitly
|
|
55
|
+
# flipped are stored, so tools added by future releases keep getting their
|
|
56
|
+
# DEFAULT (e.g. a new beta tool defaults hidden) instead of freezing the
|
|
57
|
+
# catalog shape at save time.
|
|
58
|
+
LLM_API_CONFIG_KEY = "llm_api"
|
|
59
|
+
|
|
60
|
+
# Tools hidden from conversation agents by default regardless of tags:
|
|
61
|
+
# operational hazards where an agent mistake is expensive (restart loops,
|
|
62
|
+
# config reloads mid-edit, backup churn). Users can still enable each in the
|
|
63
|
+
# settings UI.
|
|
64
|
+
LLM_API_DEFAULT_OFF_TOOLS: frozenset[str] = frozenset(
|
|
65
|
+
{
|
|
66
|
+
"ha_restart",
|
|
67
|
+
"ha_reload_core",
|
|
68
|
+
"ha_manage_backup",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Developer-mode tools are hidden by default via this name prefix (covers
|
|
73
|
+
# ha_dev_manage_settings / ha_dev_manage_server and any future ha_dev_*).
|
|
74
|
+
LLM_API_DEV_TOOL_PREFIX = "ha_dev_"
|
|
75
|
+
|
|
76
|
+
# The beta gate is tag-based so future beta tools default to hidden without
|
|
77
|
+
# touching this module.
|
|
78
|
+
_BETA_TAG = "beta"
|
|
79
|
+
|
|
80
|
+
# How long a live settings read is reused before re-reading the config file.
|
|
81
|
+
# tools/list fires once per conversation turn per agent; the TTL only matters
|
|
82
|
+
# when several agents list within the same moment.
|
|
83
|
+
_OVERRIDES_TTL_SECONDS = 2.0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def default_llm_api_exposed(name: str, tags: Sequence[str] | set[str]) -> bool:
|
|
87
|
+
"""Return the default LLM-API exposure for a tool (no user override)."""
|
|
88
|
+
if _BETA_TAG in set(tags):
|
|
89
|
+
return False
|
|
90
|
+
if name.startswith(LLM_API_DEV_TOOL_PREFIX):
|
|
91
|
+
return False
|
|
92
|
+
return name not in LLM_API_DEFAULT_OFF_TOOLS
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def effective_llm_api_exposed(
|
|
96
|
+
name: str,
|
|
97
|
+
tags: Sequence[str] | set[str],
|
|
98
|
+
overrides: Mapping[str, Any],
|
|
99
|
+
) -> bool:
|
|
100
|
+
"""Return the effective exposure: user override if set, else the default."""
|
|
101
|
+
override = overrides.get(name)
|
|
102
|
+
if isinstance(override, bool):
|
|
103
|
+
return override
|
|
104
|
+
return default_llm_api_exposed(name, tags)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def load_llm_api_overrides() -> dict[str, bool]:
|
|
108
|
+
"""Read the persisted per-tool overrides from tool_config.json (live)."""
|
|
109
|
+
# Imported here, not at module top: settings_ui imports this module for
|
|
110
|
+
# the defaults, so a top-level import would be circular.
|
|
111
|
+
from .settings_ui import load_tool_config
|
|
112
|
+
|
|
113
|
+
config = load_tool_config()
|
|
114
|
+
# load_tool_config returns whatever the JSON file parses to — guard the
|
|
115
|
+
# valid-JSON-but-not-an-object case (e.g. `[]`) instead of raising.
|
|
116
|
+
if not isinstance(config, dict):
|
|
117
|
+
return {}
|
|
118
|
+
raw = config.get(LLM_API_CONFIG_KEY, {})
|
|
119
|
+
if not isinstance(raw, dict):
|
|
120
|
+
return {}
|
|
121
|
+
return {
|
|
122
|
+
name: value
|
|
123
|
+
for name, value in raw.items()
|
|
124
|
+
if isinstance(name, str) and isinstance(value, bool)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _pinned_tool_names() -> set[str]:
|
|
129
|
+
"""Return the effective pinned set, matching the server's tool-search view.
|
|
130
|
+
|
|
131
|
+
Mirrors ``server.py::_apply_tool_search``'s always-visible computation:
|
|
132
|
+
the built-in DEFAULT_PINNED_TOOLS minus tools the user explicitly set to
|
|
133
|
+
plain "enabled" (an unpin), plus every tool the user set to "pinned"
|
|
134
|
+
(file or env). The component mirrors pinned tools directly into the
|
|
135
|
+
agent's tool list when the exposure mode is tool-search.
|
|
136
|
+
"""
|
|
137
|
+
from .settings_ui import effective_tool_config
|
|
138
|
+
from .transforms import DEFAULT_PINNED_TOOLS
|
|
139
|
+
|
|
140
|
+
states = effective_tool_config().get("tools", {})
|
|
141
|
+
pinned = {name for name, state in states.items() if state == "pinned"}
|
|
142
|
+
unpinned_defaults = {
|
|
143
|
+
name for name in DEFAULT_PINNED_TOOLS if states.get(name) == "enabled"
|
|
144
|
+
}
|
|
145
|
+
return (set(DEFAULT_PINNED_TOOLS) - unpinned_defaults) | pinned
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class LlmExposureMiddleware(Middleware):
|
|
149
|
+
"""Stamp every listed tool with its LLM-API exposure + pinned state.
|
|
150
|
+
|
|
151
|
+
Runs after the visibility layer, so globally-disabled tools are already
|
|
152
|
+
absent and never carry a stamp. The stamp is additive metadata — no tool
|
|
153
|
+
is hidden or altered for regular MCP clients.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(self) -> None:
|
|
157
|
+
"""Initialize the short-lived settings cache."""
|
|
158
|
+
self._cache: tuple[float, dict[str, bool], set[str]] | None = None
|
|
159
|
+
|
|
160
|
+
def _current_settings(self) -> tuple[dict[str, bool], set[str]]:
|
|
161
|
+
"""Return (overrides, pinned) with a short TTL over the file reads."""
|
|
162
|
+
now = time.monotonic()
|
|
163
|
+
if self._cache is not None:
|
|
164
|
+
stamp, overrides, pinned = self._cache
|
|
165
|
+
if now - stamp < _OVERRIDES_TTL_SECONDS:
|
|
166
|
+
return overrides, pinned
|
|
167
|
+
try:
|
|
168
|
+
overrides = load_llm_api_overrides()
|
|
169
|
+
pinned = _pinned_tool_names()
|
|
170
|
+
except Exception:
|
|
171
|
+
# Stamping must never break tools/list for regular clients — but
|
|
172
|
+
# the fallback direction matters for an exposure CONTROL: pure
|
|
173
|
+
# defaults would re-EXPOSE tools the user explicitly hid (their
|
|
174
|
+
# override lives in the unreadable settings), so serve the
|
|
175
|
+
# last-known-good values when we have them (review finding).
|
|
176
|
+
# Only a failure with no prior successful read stamps pure
|
|
177
|
+
# defaults. Visibly, either way.
|
|
178
|
+
if self._cache is not None:
|
|
179
|
+
_stamp, overrides, pinned = self._cache
|
|
180
|
+
logger.warning(
|
|
181
|
+
"Could not read LLM-API exposure settings; keeping the "
|
|
182
|
+
"last known values",
|
|
183
|
+
exc_info=True,
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
overrides, pinned = {}, set()
|
|
187
|
+
logger.warning(
|
|
188
|
+
"Could not read LLM-API exposure settings and no prior "
|
|
189
|
+
"read succeeded; stamping defaults",
|
|
190
|
+
exc_info=True,
|
|
191
|
+
)
|
|
192
|
+
self._cache = (now, overrides, pinned)
|
|
193
|
+
return overrides, pinned
|
|
194
|
+
|
|
195
|
+
async def on_list_tools(
|
|
196
|
+
self,
|
|
197
|
+
context: MiddlewareContext[mt.ListToolsRequest],
|
|
198
|
+
call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],
|
|
199
|
+
) -> Sequence[Tool]:
|
|
200
|
+
"""Stamp ``_meta.ha_mcp`` on every tool in the list result."""
|
|
201
|
+
tools = await call_next(context)
|
|
202
|
+
overrides, pinned = self._current_settings()
|
|
203
|
+
|
|
204
|
+
stamped: list[Tool] = []
|
|
205
|
+
for tool in tools:
|
|
206
|
+
meta = dict(tool.meta or {})
|
|
207
|
+
namespace = dict(meta.get(META_NAMESPACE) or {})
|
|
208
|
+
namespace[META_EXPOSED_KEY] = effective_llm_api_exposed(
|
|
209
|
+
tool.name, tool.tags or set(), overrides
|
|
210
|
+
)
|
|
211
|
+
namespace[META_PINNED_KEY] = tool.name in pinned
|
|
212
|
+
meta[META_NAMESPACE] = namespace
|
|
213
|
+
stamped.append(tool.model_copy(update={"meta": meta}))
|
|
214
|
+
return stamped
|
|
@@ -243,6 +243,14 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
|
|
|
243
243
|
|
|
244
244
|
self.mcp.add_middleware(ToolSearchHintMiddleware())
|
|
245
245
|
|
|
246
|
+
# Stamp every tools/list entry with its conversation-agent LLM API
|
|
247
|
+
# exposure + pinned state (#1745). Additive metadata only — regular
|
|
248
|
+
# clients are unaffected; the ha_mcp_tools custom component filters
|
|
249
|
+
# what it offers to Home Assistant conversation agents on it.
|
|
250
|
+
from .llm_exposure import LlmExposureMiddleware
|
|
251
|
+
|
|
252
|
+
self.mcp.add_middleware(LlmExposureMiddleware())
|
|
253
|
+
|
|
246
254
|
# Read Only Mode write blocker (discussion #1569) — always
|
|
247
255
|
# installed, consults the live flag per call. Before
|
|
248
256
|
# PolicyMiddleware so a write blocked by Read Only Mode never
|
|
@@ -37,6 +37,7 @@ from ..config import (
|
|
|
37
37
|
get_global_settings,
|
|
38
38
|
)
|
|
39
39
|
from ..errors import ErrorCode, create_error_response
|
|
40
|
+
from ..llm_exposure import LLM_API_CONFIG_KEY
|
|
40
41
|
from ..transforms import DEFAULT_PINNED_TOOLS, categorize_capability
|
|
41
42
|
from ..utils.data_paths import get_data_dir
|
|
42
43
|
|
|
@@ -1370,14 +1371,38 @@ def build_settings_handlers(
|
|
|
1370
1371
|
# (their write actions are blocked at call time instead). The JS
|
|
1371
1372
|
# uses this to keep their toggles live while force-disabling the
|
|
1372
1373
|
# other write-capable tools' rows when the mode is on.
|
|
1374
|
+
# Conversation-agent LLM API exposure (#1745): effective value per
|
|
1375
|
+
# tool (override else default) so the UI toggle renders the truth,
|
|
1376
|
+
# plus the raw overrides so it can tell "user-set" from "default".
|
|
1377
|
+
from ..llm_exposure import effective_llm_api_exposed, load_llm_api_overrides
|
|
1373
1378
|
from ..read_only import READ_ONLY_EXEMPT_TOOLS
|
|
1374
1379
|
|
|
1380
|
+
llm_overrides = load_llm_api_overrides()
|
|
1381
|
+
# Feature-gated stub rows carry their primary tag but NOT the "beta"
|
|
1382
|
+
# tag the registered tool declares (_render_stub renders from
|
|
1383
|
+
# FEATURE_GATED_TOOLS metadata, whose tags are never empty) — append
|
|
1384
|
+
# it whenever disabled_by is set so the toggle renders
|
|
1385
|
+
# hidden-by-default, matching what the stamp will say once the flag
|
|
1386
|
+
# turns the real tool on. Every feature-gated tool is beta by
|
|
1387
|
+
# definition. (Review finding: a previous `or`-fallback here was dead
|
|
1388
|
+
# code, so beta stubs rendered as exposed.)
|
|
1389
|
+
llm_effective = {
|
|
1390
|
+
t["name"]: effective_llm_api_exposed(
|
|
1391
|
+
t["name"],
|
|
1392
|
+
[*(t.get("tags") or []), *(["beta"] if t.get("disabled_by") else [])],
|
|
1393
|
+
llm_overrides,
|
|
1394
|
+
)
|
|
1395
|
+
for t in tools
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1375
1398
|
return JSONResponse(
|
|
1376
1399
|
{
|
|
1377
1400
|
"tools": tools,
|
|
1378
1401
|
"states": states,
|
|
1379
1402
|
"env_pinned": pinned,
|
|
1380
1403
|
"read_only_exempt": sorted(READ_ONLY_EXEMPT_TOOLS),
|
|
1404
|
+
"llm_api": llm_effective,
|
|
1405
|
+
"llm_api_overrides": llm_overrides,
|
|
1381
1406
|
}
|
|
1382
1407
|
)
|
|
1383
1408
|
|
|
@@ -1462,8 +1487,43 @@ def build_settings_handlers(
|
|
|
1462
1487
|
name: state for name, state in states.items() if name not in env_pinned
|
|
1463
1488
|
}
|
|
1464
1489
|
|
|
1490
|
+
# Conversation-agent LLM API exposure overrides (#1745): a sparse
|
|
1491
|
+
# {tool_name: bool} map, orthogonal to the states enum. Only bools
|
|
1492
|
+
# persist — tools the user never flipped keep tracking their
|
|
1493
|
+
# (deny-by-default for beta/dev/restart) defaults across releases.
|
|
1494
|
+
raw_llm_api = body.get("llm_api", {})
|
|
1495
|
+
if not isinstance(raw_llm_api, dict):
|
|
1496
|
+
return JSONResponse(
|
|
1497
|
+
create_error_response(
|
|
1498
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
1499
|
+
"'llm_api' must be an object mapping tool names to booleans",
|
|
1500
|
+
),
|
|
1501
|
+
status_code=400,
|
|
1502
|
+
)
|
|
1503
|
+
llm_api_overrides = {
|
|
1504
|
+
name: value
|
|
1505
|
+
for name, value in raw_llm_api.items()
|
|
1506
|
+
if isinstance(name, str) and isinstance(value, bool)
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1465
1509
|
config = load_tool_config()
|
|
1510
|
+
|
|
1511
|
+
# The enable/disable/pin half still needs a restart to apply
|
|
1512
|
+
# (visibility is wired at server build); the LLM-API exposure half
|
|
1513
|
+
# applies live (stamped per tools/list). Only demand a restart when
|
|
1514
|
+
# the half that needs one actually changed. Compare with the SAME
|
|
1515
|
+
# default-pinned padding _get_tools applies to its response — the JS
|
|
1516
|
+
# posts that padded map back verbatim, so an unpadded compare would
|
|
1517
|
+
# flag every first save as a states change (live-found on #1745).
|
|
1518
|
+
def _padded(tool_states: dict[str, str]) -> dict[str, str]:
|
|
1519
|
+
padded = dict(tool_states)
|
|
1520
|
+
for name in DEFAULT_PINNED_TOOLS:
|
|
1521
|
+
padded.setdefault(name, "pinned")
|
|
1522
|
+
return padded
|
|
1523
|
+
|
|
1524
|
+
states_changed = _padded(config.get("tools", {})) != _padded(states)
|
|
1466
1525
|
config["tools"] = states
|
|
1526
|
+
config[LLM_API_CONFIG_KEY] = llm_api_overrides
|
|
1467
1527
|
if not save_tool_config(config):
|
|
1468
1528
|
return JSONResponse(
|
|
1469
1529
|
create_error_response(
|
|
@@ -1480,9 +1540,13 @@ def build_settings_handlers(
|
|
|
1480
1540
|
disabled_count = sum(1 for s in states.values() if s == "disabled")
|
|
1481
1541
|
pinned_count = sum(1 for s in states.values() if s == "pinned")
|
|
1482
1542
|
logger.info(
|
|
1483
|
-
"Saved tool config (
|
|
1543
|
+
"Saved tool config (%s): %d disabled, %d pinned, %d LLM-API overrides",
|
|
1544
|
+
"restart required to apply"
|
|
1545
|
+
if states_changed
|
|
1546
|
+
else "LLM-API exposure applies live",
|
|
1484
1547
|
disabled_count,
|
|
1485
1548
|
pinned_count,
|
|
1549
|
+
len(llm_api_overrides),
|
|
1486
1550
|
)
|
|
1487
1551
|
|
|
1488
1552
|
# Same response shape as ``_save_feature_flags`` and
|
|
@@ -1496,8 +1560,9 @@ def build_settings_handlers(
|
|
|
1496
1560
|
{
|
|
1497
1561
|
"success": True,
|
|
1498
1562
|
"applied": states,
|
|
1563
|
+
"llm_api_applied": llm_api_overrides,
|
|
1499
1564
|
"mode": "file",
|
|
1500
|
-
"restart_required":
|
|
1565
|
+
"restart_required": states_changed,
|
|
1501
1566
|
}
|
|
1502
1567
|
)
|
|
1503
1568
|
|
|
@@ -26,6 +26,13 @@ let toolStates = {};
|
|
|
26
26
|
// Populated from data.env_pinned in loadTools(); read by render() to
|
|
27
27
|
// lock rows and show the env-var name banner.
|
|
28
28
|
let toolEnvPinned = {};
|
|
29
|
+
// Conversation-agent LLM API exposure (#1745). toolLlm mirrors the
|
|
30
|
+
// server-computed EFFECTIVE value per tool (user override, else the
|
|
31
|
+
// deny-by-default for beta/dev/restart tools); toolLlmOverrides holds
|
|
32
|
+
// only the user-set overrides and is what saveConfig persists — tools
|
|
33
|
+
// never flipped keep tracking their defaults across releases.
|
|
34
|
+
let toolLlm = {};
|
|
35
|
+
let toolLlmOverrides = {};
|
|
29
36
|
let saveTimer = null;
|
|
30
37
|
let openGroups = new Set();
|
|
31
38
|
|
|
@@ -160,6 +167,8 @@ async function loadTools() {
|
|
|
160
167
|
toolData = data.tools || [];
|
|
161
168
|
toolStates = data.states || {};
|
|
162
169
|
toolEnvPinned = data.env_pinned || {};
|
|
170
|
+
toolLlm = data.llm_api || {};
|
|
171
|
+
toolLlmOverrides = data.llm_api_overrides || {};
|
|
163
172
|
READ_ONLY_EXEMPT = new Set(data.read_only_exempt || []);
|
|
164
173
|
// Load policy state before the first render so the "security gated"
|
|
165
174
|
// toggle reflects current policy.rules. loadPolicyState() never throws
|
|
@@ -741,6 +750,14 @@ function render() {
|
|
|
741
750
|
`<span class="slider"></span></label>` +
|
|
742
751
|
`<span>security gated</span>` +
|
|
743
752
|
`</div>` +
|
|
753
|
+
`<div class="toggle-group ${isEnabled ? '' : 'disabled-toggle'}" ` +
|
|
754
|
+
`title="Offer this tool to Home Assistant conversation agents through the LLM API. Applies on the agent's next message - no restart. A tool disabled above is unavailable to agents regardless.">` +
|
|
755
|
+
`<label class="switch"><input type="checkbox" name="tool:${escapeHtml(t.name)}:llm" data-tool="${escapeHtml(t.name)}" data-field="llm" ` +
|
|
756
|
+
`aria-label="${escapeHtml(title)} exposed to the conversation-agent LLM API" ` +
|
|
757
|
+
`${(toolLlm[t.name] !== false) ? 'checked' : ''} ${isEnabled ? '' : 'disabled'}>` +
|
|
758
|
+
`<span class="slider"></span></label>` +
|
|
759
|
+
`<span>LLM API</span>` +
|
|
760
|
+
`</div>` +
|
|
744
761
|
`</div>`;
|
|
745
762
|
|
|
746
763
|
const inputs = div.querySelectorAll('input[type="checkbox"]');
|
|
@@ -766,6 +783,16 @@ function render() {
|
|
|
766
783
|
render();
|
|
767
784
|
return;
|
|
768
785
|
}
|
|
786
|
+
if (field === 'llm') {
|
|
787
|
+
// LLM-API exposure lives in its own overrides map (persisted
|
|
788
|
+
// alongside states by saveConfig); the effective map mirrors it
|
|
789
|
+
// immediately so the re-render shows the new value.
|
|
790
|
+
toolLlm[t.name] = e.target.checked;
|
|
791
|
+
toolLlmOverrides[t.name] = e.target.checked;
|
|
792
|
+
scheduleSave();
|
|
793
|
+
render();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
769
796
|
const currentState = getState(t.name);
|
|
770
797
|
let newState = currentState;
|
|
771
798
|
if (field === 'enabled') {
|
|
@@ -816,7 +843,7 @@ async function saveConfig() {
|
|
|
816
843
|
resp = await fetch('./api/settings/tools', {
|
|
817
844
|
method: 'POST',
|
|
818
845
|
headers: {'Content-Type': 'application/json'},
|
|
819
|
-
body: JSON.stringify({states: toolStates}),
|
|
846
|
+
body: JSON.stringify({states: toolStates, llm_api: toolLlmOverrides}),
|
|
820
847
|
});
|
|
821
848
|
} catch (e) {
|
|
822
849
|
// Auto-save is fire-and-forget (scheduleSave -> setTimeout); without
|
|
@@ -826,8 +853,20 @@ async function saveConfig() {
|
|
|
826
853
|
return;
|
|
827
854
|
}
|
|
828
855
|
if (resp.ok) {
|
|
829
|
-
|
|
830
|
-
|
|
856
|
+
// LLM-API-exposure-only saves apply live (stamped per tools/list);
|
|
857
|
+
// enable/disable/pin changes still need a restart. The server tells
|
|
858
|
+
// us which happened.
|
|
859
|
+
let restartRequired = true;
|
|
860
|
+
try {
|
|
861
|
+
const saved = await resp.json();
|
|
862
|
+
restartRequired = saved.restart_required !== false;
|
|
863
|
+
} catch (e) { /* keep the conservative default */ }
|
|
864
|
+
if (restartRequired) {
|
|
865
|
+
updateStatus('Saved. Restart required.', true);
|
|
866
|
+
markRestartRequired();
|
|
867
|
+
} else {
|
|
868
|
+
updateStatus('Saved. LLM API exposure applies on the next agent message.', true);
|
|
869
|
+
}
|
|
831
870
|
} else {
|
|
832
871
|
// Surface the server's structured error when present (mirrors
|
|
833
872
|
// saveAdvancedSettings / saveFeatureFlag) instead of a generic
|
{ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/tools/config_entry_flow.py
RENAMED
|
@@ -165,6 +165,28 @@ def _record_ignored_section_keys(
|
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def _ignored_keys_warnings(
|
|
169
|
+
ignored_config_keys: set[str], remaining_config: dict[str, Any]
|
|
170
|
+
) -> list[str]:
|
|
171
|
+
"""Build warnings for caller-supplied config keys no flow step consumed."""
|
|
172
|
+
warnings: list[str] = []
|
|
173
|
+
ignored = ignored_config_keys | {
|
|
174
|
+
key for key in remaining_config if key not in _MENU_SELECTION_KEYS
|
|
175
|
+
}
|
|
176
|
+
if ignored:
|
|
177
|
+
warnings.append(
|
|
178
|
+
"Ignored config keys not declared by the Home Assistant flow "
|
|
179
|
+
f"schema: {', '.join(sorted(ignored))}"
|
|
180
|
+
)
|
|
181
|
+
leftover_menu_keys = _MENU_SELECTION_KEYS & remaining_config.keys()
|
|
182
|
+
if leftover_menu_keys:
|
|
183
|
+
warnings.append(
|
|
184
|
+
"Ignored menu selection key(s) with no matching menu step: "
|
|
185
|
+
f"{', '.join(sorted(leftover_menu_keys))}"
|
|
186
|
+
)
|
|
187
|
+
return warnings
|
|
188
|
+
|
|
189
|
+
|
|
168
190
|
def _consume_form_schema(
|
|
169
191
|
data_schema: list[Any],
|
|
170
192
|
remaining_config: dict[str, Any],
|
|
@@ -643,15 +665,10 @@ async def _handle_flow_steps(
|
|
|
643
665
|
result_type = current_step.get("type")
|
|
644
666
|
|
|
645
667
|
if result_type == _FlowType.CREATE_ENTRY:
|
|
646
|
-
ignored_config_keys.update(
|
|
647
|
-
key for key in remaining_config if key not in _MENU_SELECTION_KEYS
|
|
648
|
-
)
|
|
649
668
|
response: dict[str, Any] = {"success": True, "entry": current_step}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
f"schema: {', '.join(sorted(ignored_config_keys))}"
|
|
654
|
-
]
|
|
669
|
+
warnings = _ignored_keys_warnings(ignored_config_keys, remaining_config)
|
|
670
|
+
if warnings:
|
|
671
|
+
response["warnings"] = warnings
|
|
655
672
|
return response
|
|
656
673
|
|
|
657
674
|
if result_type == _FlowType.ABORT:
|
|
@@ -731,30 +748,43 @@ async def _handle_config_subentry_flow_steps(
|
|
|
731
748
|
*,
|
|
732
749
|
is_reconfigure: bool,
|
|
733
750
|
) -> dict[str, Any]:
|
|
734
|
-
"""Walk a config subentry flow and accept HA's reconfigure-success abort.
|
|
751
|
+
"""Walk a config subentry flow and accept HA's reconfigure-success abort.
|
|
752
|
+
|
|
753
|
+
Successful results include ``warnings`` when caller-supplied config keys
|
|
754
|
+
were not declared by any flow step.
|
|
755
|
+
"""
|
|
735
756
|
remaining_config = dict(config)
|
|
736
757
|
current_step = initial_step
|
|
737
758
|
last_menu_choice: str | None = None
|
|
759
|
+
ignored_config_keys: set[str] = set()
|
|
738
760
|
max_steps = 10
|
|
739
761
|
|
|
740
762
|
for step_num in range(max_steps):
|
|
741
763
|
result_type = current_step.get("type")
|
|
742
764
|
|
|
743
765
|
if result_type == _FlowType.CREATE_ENTRY:
|
|
744
|
-
|
|
766
|
+
response: dict[str, Any] = {
|
|
745
767
|
"success": True,
|
|
746
768
|
"operation": "created",
|
|
747
769
|
"flow_result": current_step,
|
|
748
770
|
}
|
|
771
|
+
warnings = _ignored_keys_warnings(ignored_config_keys, remaining_config)
|
|
772
|
+
if warnings:
|
|
773
|
+
response["warnings"] = warnings
|
|
774
|
+
return response
|
|
749
775
|
|
|
750
776
|
if result_type == _FlowType.ABORT:
|
|
751
777
|
reason = current_step.get("reason")
|
|
752
778
|
if is_reconfigure and reason in _RECONFIGURE_SUCCESS_REASONS:
|
|
753
|
-
|
|
779
|
+
response = {
|
|
754
780
|
"success": True,
|
|
755
781
|
"operation": "reconfigured",
|
|
756
782
|
"flow_result": current_step,
|
|
757
783
|
}
|
|
784
|
+
warnings = _ignored_keys_warnings(ignored_config_keys, remaining_config)
|
|
785
|
+
if warnings:
|
|
786
|
+
response["warnings"] = warnings
|
|
787
|
+
return response
|
|
758
788
|
raise_tool_error(
|
|
759
789
|
create_error_response(
|
|
760
790
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
@@ -784,7 +814,12 @@ async def _handle_config_subentry_flow_steps(
|
|
|
784
814
|
continue
|
|
785
815
|
|
|
786
816
|
if result_type == _FlowType.FORM:
|
|
787
|
-
form_data = _handle_form_step(
|
|
817
|
+
form_data = _handle_form_step(
|
|
818
|
+
flow_id,
|
|
819
|
+
current_step,
|
|
820
|
+
remaining_config,
|
|
821
|
+
ignored_config_keys,
|
|
822
|
+
)
|
|
788
823
|
logger.debug(
|
|
789
824
|
"Config subentry flow step %s: form submit (step_id=%s, keys=%s)",
|
|
790
825
|
step_num,
|
|
@@ -895,7 +930,7 @@ async def set_config_subentry(
|
|
|
895
930
|
)
|
|
896
931
|
raise
|
|
897
932
|
|
|
898
|
-
|
|
933
|
+
response = {
|
|
899
934
|
"success": True,
|
|
900
935
|
"entry_id": entry_id,
|
|
901
936
|
"subentry_type": subentry_type,
|
|
@@ -904,6 +939,9 @@ async def set_config_subentry(
|
|
|
904
939
|
"flow_result": result["flow_result"],
|
|
905
940
|
"message": f"Config subentry {result['operation']} successfully",
|
|
906
941
|
}
|
|
942
|
+
if result.get("warnings"):
|
|
943
|
+
response["warnings"] = result["warnings"]
|
|
944
|
+
return response
|
|
907
945
|
|
|
908
946
|
|
|
909
947
|
async def get_user_step_field_names(client: Any, helper_type: str) -> set[str] | None:
|
|
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.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/client/supervisor_client.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/client/websocket_client.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/client/websocket_listener.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/dashboard_screenshot/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1867}/src/ha_mcp/dashboard_screenshot/capture.py
RENAMED
|
File without changes
|