ha-mcp-dev 7.6.0.dev622__tar.gz → 7.6.0.dev624__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.6.0.dev622/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev624}/PKG-INFO +1 -1
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/pyproject.toml +1 -1
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/config.py +55 -8
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/server.py +6 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/settings_ui.py +162 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_filesystem.py +1 -1
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_yaml_config.py +85 -0
- ha_mcp_dev-7.6.0.dev624/src/ha_mcp/tools/validation_middleware.py +57 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/setup.cfg +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/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.6.0.
|
|
7
|
+
version = "7.6.0.dev624"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13,<3.14"
|
|
@@ -110,7 +110,7 @@ class Settings(BaseSettings):
|
|
|
110
110
|
|
|
111
111
|
# Master beta-features toggle. UI-only — intentionally not in any
|
|
112
112
|
# addon config.yaml schema. Consumed by the master gate in
|
|
113
|
-
# ``_apply_feature_flag_overrides``, which force-sets the
|
|
113
|
+
# ``_apply_feature_flag_overrides``, which force-sets the
|
|
114
114
|
# ``BETA_FEATURE_FIELDS`` sub-flags to False whenever this master is
|
|
115
115
|
# off. Dev addon ``start.py`` auto-writes ``ENABLE_BETA_FEATURES=true``
|
|
116
116
|
# whenever any beta sub-flag key is present in ``/data/options.json``
|
|
@@ -122,6 +122,27 @@ class Settings(BaseSettings):
|
|
|
122
122
|
# files. Disabled by default; only for YAML-only features with no UI/API path.
|
|
123
123
|
enable_yaml_config_editing: bool = Field(False, alias="ENABLE_YAML_CONFIG_EDITING")
|
|
124
124
|
|
|
125
|
+
# Per-key gates for ``automation`` / ``script`` / ``scene`` under
|
|
126
|
+
# ``packages/*.yaml``. The custom component accepts these three
|
|
127
|
+
# PACKAGES_ONLY_YAML_KEYS unconditionally; ha-mcp's UI exposes a
|
|
128
|
+
# toggle per key so an operator who wants YAML-managed
|
|
129
|
+
# automations/scripts/scenes in packages but not the others can
|
|
130
|
+
# narrow the surface. ha_config_set_yaml rejects packages/*.yaml
|
|
131
|
+
# writes for a disabled key client-side, and passes the disabled set
|
|
132
|
+
# to the custom component so the underlying service rejects too
|
|
133
|
+
# (writes of these keys to configuration.yaml are rejected
|
|
134
|
+
# independently of these flags). Each
|
|
135
|
+
# toggle is meaningful only when ``enable_yaml_config_editing`` is
|
|
136
|
+
# on; the UI nests these rows under that parent and dims them when
|
|
137
|
+
# the parent is off.
|
|
138
|
+
enable_yaml_packages_automation: bool = Field(
|
|
139
|
+
False, alias="ENABLE_YAML_PACKAGES_AUTOMATION"
|
|
140
|
+
)
|
|
141
|
+
enable_yaml_packages_script: bool = Field(
|
|
142
|
+
False, alias="ENABLE_YAML_PACKAGES_SCRIPT"
|
|
143
|
+
)
|
|
144
|
+
enable_yaml_packages_scene: bool = Field(False, alias="ENABLE_YAML_PACKAGES_SCENE")
|
|
145
|
+
|
|
125
146
|
# Seed values for tool visibility (comma-separated tool names).
|
|
126
147
|
# Used as initial config when no tool_config.json exists.
|
|
127
148
|
# The web settings UI (/settings) is the primary interface for managing these.
|
|
@@ -431,6 +452,21 @@ FEATURE_FLAG_FIELDS: tuple[FeatureFlagField, ...] = (
|
|
|
431
452
|
# are name-disjoint per _validate_registries()).
|
|
432
453
|
FeatureFlagField("enable_mandatory_bps", "ENABLE_MANDATORY_BPS", bool),
|
|
433
454
|
FeatureFlagField("enable_yaml_config_editing", "ENABLE_YAML_CONFIG_EDITING", bool),
|
|
455
|
+
# Per-key sub-gates beneath enable_yaml_config_editing. Nested in
|
|
456
|
+
# the UI, dimmed when the parent is off. Also listed in
|
|
457
|
+
# BETA_FEATURE_FIELDS so they follow the same master-gate +
|
|
458
|
+
# addon-mode override path as the other beta flags — that is what
|
|
459
|
+
# makes the web-UI toggle take effect on the stable add-on (where
|
|
460
|
+
# they are not in config.yaml). See that tuple for the rationale.
|
|
461
|
+
FeatureFlagField(
|
|
462
|
+
"enable_yaml_packages_automation",
|
|
463
|
+
"ENABLE_YAML_PACKAGES_AUTOMATION",
|
|
464
|
+
bool,
|
|
465
|
+
),
|
|
466
|
+
FeatureFlagField(
|
|
467
|
+
"enable_yaml_packages_script", "ENABLE_YAML_PACKAGES_SCRIPT", bool
|
|
468
|
+
),
|
|
469
|
+
FeatureFlagField("enable_yaml_packages_scene", "ENABLE_YAML_PACKAGES_SCENE", bool),
|
|
434
470
|
FeatureFlagField("enable_lite_docstrings", "ENABLE_LITE_DOCSTRINGS", bool),
|
|
435
471
|
FeatureFlagField("enable_filesystem_tools", "HAMCP_ENABLE_FILESYSTEM_TOOLS", bool),
|
|
436
472
|
FeatureFlagField(
|
|
@@ -467,6 +503,16 @@ _FEATURE_FLAG_INT_BOUNDS: dict[str, tuple[int, int]] = {
|
|
|
467
503
|
# gate, never by the per-field iteration.
|
|
468
504
|
BETA_FEATURE_FIELDS: tuple[str, ...] = (
|
|
469
505
|
"enable_yaml_config_editing",
|
|
506
|
+
# Per-key sub-gates of enable_yaml_config_editing. Included here so
|
|
507
|
+
# they ride the same master gate + addon-mode override path as the
|
|
508
|
+
# other beta flags. Without this, the addon-mode short-circuit in
|
|
509
|
+
# ``_apply_feature_flag_overrides`` (and the ``get_feature_flag_origin``
|
|
510
|
+
# logic) would leave them dead on the stable add-on — reachable only
|
|
511
|
+
# via the dev add-on's config.yaml options. They still render NESTED
|
|
512
|
+
# under their parent in the web UI (not as separate beta-sub rows).
|
|
513
|
+
"enable_yaml_packages_automation",
|
|
514
|
+
"enable_yaml_packages_script",
|
|
515
|
+
"enable_yaml_packages_scene",
|
|
470
516
|
"enable_filesystem_tools",
|
|
471
517
|
"enable_custom_component_integration",
|
|
472
518
|
"enable_code_mode",
|
|
@@ -479,8 +525,9 @@ BETA_FEATURE_FIELDS: tuple[str, ...] = (
|
|
|
479
525
|
#
|
|
480
526
|
# - ``section`` groups fields in the Advanced section of the Server Settings
|
|
481
527
|
# tab: "connection", "search", "operations", "diagnostics", "tools_surface".
|
|
482
|
-
# The
|
|
483
|
-
# the UI renders below the Advanced section
|
|
528
|
+
# The beta sub-flags + the master live in a separate "beta" section that
|
|
529
|
+
# the UI renders below the Advanced section (the per-key yaml-packages
|
|
530
|
+
# sub-flags render nested under enable_yaml_config_editing within it).
|
|
484
531
|
# - ``editable=False`` marks display-only rows. Connection fields are
|
|
485
532
|
# non-editable from the running server (chicken-and-egg footgun);
|
|
486
533
|
# ``MCP_SERVER_VERSION`` is editable (it has an env alias) but the UI
|
|
@@ -631,7 +678,7 @@ def get_feature_flag_origin(env_name: str) -> str:
|
|
|
631
678
|
schema, ``start.py`` doesn't write the env var, and the master
|
|
632
679
|
falls through to env / file / default precedence so the
|
|
633
680
|
standalone web UI master path remains the gate.
|
|
634
|
-
- The
|
|
681
|
+
- The ``BETA_FEATURE_FIELDS`` (sub-flags) follow the same
|
|
635
682
|
shape — present in dev addon schema, absent from stable. Same
|
|
636
683
|
env-var-presence signal distinguishes them at runtime.
|
|
637
684
|
"""
|
|
@@ -650,8 +697,8 @@ def get_feature_flag_origin(env_name: str) -> str:
|
|
|
650
697
|
# Stable addon: env var never written → fall through to
|
|
651
698
|
# file/default. The master moved from "never schema-bound"
|
|
652
699
|
# to "schema-bound on dev only"; the same env-var-presence
|
|
653
|
-
# signal now distinguishes both for the master and the
|
|
654
|
-
# sub-flags.
|
|
700
|
+
# signal now distinguishes both for the master and the
|
|
701
|
+
# beta sub-flags.
|
|
655
702
|
if os.environ.get(env_name) is not None:
|
|
656
703
|
return "addon"
|
|
657
704
|
# else: stable / legacy-dev-no-master-key, fall through.
|
|
@@ -748,7 +795,7 @@ def _apply_feature_flag_overrides(settings: "Settings") -> None:
|
|
|
748
795
|
|
|
749
796
|
EXCEPTION: the beta-master + beta-sub-flag fields skip the
|
|
750
797
|
addon-mode short-circuit. The master isn't in any addon schema;
|
|
751
|
-
the
|
|
798
|
+
the sub-flags are in the dev-addon schema (where ``start.py``
|
|
752
799
|
writes the env var from options.json — env-var-wins skips the
|
|
753
800
|
file read here, leaving Supervisor authoritative) but NOT in the
|
|
754
801
|
stable schema (where the env var is never written, so the file
|
|
@@ -756,7 +803,7 @@ def _apply_feature_flag_overrides(settings: "Settings") -> None:
|
|
|
756
803
|
|
|
757
804
|
2. **Beta master gate**: after the per-field pass, if
|
|
758
805
|
``enable_beta_features`` is False on the resolved Settings,
|
|
759
|
-
force-set the
|
|
806
|
+
force-set the BETA_FEATURE_FIELDS to False regardless of
|
|
760
807
|
how they currently look. This is the "master toggle" semantics —
|
|
761
808
|
even a power user who sets ENABLE_YAML_CONFIG_EDITING=true via
|
|
762
809
|
env var still needs to flip the master before the flag takes
|
|
@@ -190,6 +190,12 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
|
|
|
190
190
|
# the skill guide tool are registered so it can wrap everything)
|
|
191
191
|
self._apply_tool_search()
|
|
192
192
|
|
|
193
|
+
# Convert Pydantic type-validation errors to structured ToolErrors so
|
|
194
|
+
# models get actionable guidance instead of raw Pydantic messages.
|
|
195
|
+
from .tools.validation_middleware import ValidationErrorMiddleware
|
|
196
|
+
|
|
197
|
+
self.mcp.add_middleware(ValidationErrorMiddleware())
|
|
198
|
+
|
|
193
199
|
# Wire tool security policies middleware (#966) — opt-in via
|
|
194
200
|
# ENABLE_TOOL_SECURITY_POLICIES. Must come last so the middleware
|
|
195
201
|
# wraps the final tool surface (including the search proxies).
|
|
@@ -814,6 +814,15 @@ _SETTINGS_HTML = (
|
|
|
814
814
|
left: 36px; top: 0; bottom: 0; width: 2px; background: var(--border); }
|
|
815
815
|
.feature-row.codemode-sub.dimmed { opacity: 0.55; }
|
|
816
816
|
.feature-row.codemode-sub.dimmed input { cursor: not-allowed; }
|
|
817
|
+
/* YAML packages per-key sub-rows — second-level nested under
|
|
818
|
+
enable_yaml_config_editing. Same dimming logic as codemode-sub
|
|
819
|
+
but gated by enable_yaml_config_editing (the parent flag) rather
|
|
820
|
+
than enable_code_mode. */
|
|
821
|
+
.feature-row.yaml-packages-sub { padding-left: 56px; position: relative; }
|
|
822
|
+
.feature-row.yaml-packages-sub::before { content: ""; position: absolute;
|
|
823
|
+
left: 36px; top: 0; bottom: 0; width: 2px; background: var(--border); }
|
|
824
|
+
.feature-row.yaml-packages-sub.dimmed { opacity: 0.55; }
|
|
825
|
+
.feature-row.yaml-packages-sub.dimmed input { cursor: not-allowed; }
|
|
817
826
|
/* Advanced settings sections — one row per
|
|
818
827
|
ADVANCED_SETTINGS_FIELDS entry, grouped by section. Visually
|
|
819
828
|
matches the .feature-row treatment so the Server Settings tab
|
|
@@ -2180,6 +2189,18 @@ const FEATURE_META = {
|
|
|
2180
2189
|
label: "Enable YAML config editing (beta)",
|
|
2181
2190
|
help: "Beta feature — disabled by default. Allows AI assistants to add, replace, or remove top-level keys in configuration.yaml and packages/*.yaml. Only whitelisted keys are allowed (e.g., template, sensor, command_line, mqtt, knx); core keys like homeassistant, http, and recorder are blocked. Each edit validates YAML syntax, runs a config check, and creates an automatic backup. Changes to most keys require a full HA restart to take effect. See docs/beta.md for known limitations. Dedicated tools (automations, scripts, scenes, helpers, template sensors) should be preferred when available.",
|
|
2182
2191
|
},
|
|
2192
|
+
enable_yaml_packages_automation: {
|
|
2193
|
+
label: "Allow automation in packages/*.yaml",
|
|
2194
|
+
help: "Sub-toggle of YAML config editing. When on, ha_config_set_yaml accepts yaml_path='automation' inside packages/*.yaml. When off, the wrapper rejects the call client-side AND the custom component rejects it server-side. Storage-mode tools (ha_config_set_automation) cover the UI-managed path and are unaffected. Disabled by default.",
|
|
2195
|
+
},
|
|
2196
|
+
enable_yaml_packages_script: {
|
|
2197
|
+
label: "Allow script in packages/*.yaml",
|
|
2198
|
+
help: "Sub-toggle of YAML config editing. When on, ha_config_set_yaml accepts yaml_path='script' inside packages/*.yaml. When off, the wrapper rejects the call client-side AND the custom component rejects it server-side. Storage-mode tools (ha_config_set_script) cover the UI-managed path and are unaffected. Disabled by default.",
|
|
2199
|
+
},
|
|
2200
|
+
enable_yaml_packages_scene: {
|
|
2201
|
+
label: "Allow scene in packages/*.yaml",
|
|
2202
|
+
help: "Sub-toggle of YAML config editing. When on, ha_config_set_yaml accepts yaml_path='scene' inside packages/*.yaml. When off, the wrapper rejects the call client-side AND the custom component rejects it server-side. Storage-mode tools (ha_config_set_scene) cover the UI-managed path and are unaffected. Disabled by default.",
|
|
2203
|
+
},
|
|
2183
2204
|
enable_filesystem_tools: {
|
|
2184
2205
|
label: "Enable filesystem tools (beta)",
|
|
2185
2206
|
help: "Sets HAMCP_ENABLE_FILESYSTEM_TOOLS=true. Enables direct file read/write access to your Home Assistant filesystem. WARNING: This gives the MCP server sensitive direct file access to your system. Only enable if you trust the AI assistant with file operations. Requires restart to take effect.",
|
|
@@ -2204,6 +2225,16 @@ const FEATURE_META = {
|
|
|
2204
2225
|
// ``config.BETA_FEATURE_FIELDS`` without duplicating the name list here.
|
|
2205
2226
|
let BETA_SUB_FLAGS = new Set();
|
|
2206
2227
|
|
|
2228
|
+
// Sub-flags of ``enable_yaml_config_editing``. Rendered nested beneath
|
|
2229
|
+
// the parent in renderFeatureFlags so the dependency is visually
|
|
2230
|
+
// obvious. They are NOT in BETA_SUB_FLAGS — the parent is the gate,
|
|
2231
|
+
// and the master-off → parent-off cascade transitively covers them.
|
|
2232
|
+
const YAML_PACKAGES_SUB_FLAGS = [
|
|
2233
|
+
'enable_yaml_packages_automation',
|
|
2234
|
+
'enable_yaml_packages_script',
|
|
2235
|
+
'enable_yaml_packages_scene',
|
|
2236
|
+
];
|
|
2237
|
+
|
|
2207
2238
|
// Cached add-on flag. Each settings endpoint (/api/settings/features,
|
|
2208
2239
|
// /api/settings/advanced, /api/settings/backup-config) returns
|
|
2209
2240
|
// ``is_addon`` so the env-locked banner copy can adapt — the addon
|
|
@@ -2313,6 +2344,10 @@ function renderFeatureFlags(flags) {
|
|
|
2313
2344
|
Object.keys(FEATURE_META).forEach(fieldName => {
|
|
2314
2345
|
const f = flags[fieldName];
|
|
2315
2346
|
if (!f) return;
|
|
2347
|
+
// Skip yaml-packages sub-rows in the main pass — they're rendered
|
|
2348
|
+
// by renderYamlPackagesSubRows below right after their parent so
|
|
2349
|
+
// the nesting reads in source order.
|
|
2350
|
+
if (YAML_PACKAGES_SUB_FLAGS.includes(fieldName)) return;
|
|
2316
2351
|
const meta = FEATURE_META[fieldName];
|
|
2317
2352
|
const isMaster = fieldName === 'enable_beta_features';
|
|
2318
2353
|
const isBetaSub = BETA_SUB_FLAGS.has(fieldName);
|
|
@@ -2381,6 +2416,20 @@ function renderFeatureFlags(flags) {
|
|
|
2381
2416
|
renderFeatureFlags(_lastFeatureFlags);
|
|
2382
2417
|
}
|
|
2383
2418
|
}
|
|
2419
|
+
// Re-render on enable_yaml_config_editing flip so the 3
|
|
2420
|
+
// packages sub-rows dim/undim immediately. Same pattern as the
|
|
2421
|
+
// master flip above — value is mutated in the live cache and
|
|
2422
|
+
// the panel re-renders synchronously while the save POST runs
|
|
2423
|
+
// in the background.
|
|
2424
|
+
if (fieldName === 'enable_yaml_config_editing') {
|
|
2425
|
+
if (_lastFeatureFlags[fieldName]) {
|
|
2426
|
+
_lastFeatureFlags[fieldName] = {
|
|
2427
|
+
..._lastFeatureFlags[fieldName],
|
|
2428
|
+
value: input.checked,
|
|
2429
|
+
};
|
|
2430
|
+
renderFeatureFlags(_lastFeatureFlags);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2384
2433
|
saveFeatureFlag(fieldName, input.checked);
|
|
2385
2434
|
});
|
|
2386
2435
|
const slider = document.createElement('span');
|
|
@@ -2417,6 +2466,74 @@ function renderFeatureFlags(flags) {
|
|
|
2417
2466
|
const codeModeOn = !!f.value;
|
|
2418
2467
|
renderCodeModeSubRows(targetBody, masterOn, codeModeOn);
|
|
2419
2468
|
}
|
|
2469
|
+
// After rendering the enable_yaml_config_editing parent, inject
|
|
2470
|
+
// its 3 per-key sub-rows (automation/script/scene). Dimmed when
|
|
2471
|
+
// either the master beta is off (parent forced off) or the parent
|
|
2472
|
+
// itself is off.
|
|
2473
|
+
if (fieldName === 'enable_yaml_config_editing') {
|
|
2474
|
+
const parentOn = !!f.value;
|
|
2475
|
+
renderYamlPackagesSubRows(flags, targetBody, masterOn, parentOn);
|
|
2476
|
+
}
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
function renderYamlPackagesSubRows(flags, parentEl, masterOn, parentOn) {
|
|
2481
|
+
YAML_PACKAGES_SUB_FLAGS.forEach(fieldName => {
|
|
2482
|
+
const f = flags[fieldName];
|
|
2483
|
+
if (!f) return;
|
|
2484
|
+
const meta = FEATURE_META[fieldName] || { label: fieldName, help: '' };
|
|
2485
|
+
const lockedByGate = !masterOn || !parentOn;
|
|
2486
|
+
const row = document.createElement('div');
|
|
2487
|
+
row.className = 'feature-row yaml-packages-sub' + (lockedByGate ? ' dimmed' : '');
|
|
2488
|
+
|
|
2489
|
+
const info = document.createElement('div');
|
|
2490
|
+
info.className = 'feature-info';
|
|
2491
|
+
const lockedNote = !f.editable
|
|
2492
|
+
? `<div class="feature-locked-note">` +
|
|
2493
|
+
(f.origin === 'env'
|
|
2494
|
+
? envLockedNoteHtml(f.env_var, fieldName)
|
|
2495
|
+
: escapeHtml(ORIGIN_LOCKED_NOTE[f.origin] || '')) +
|
|
2496
|
+
`</div>`
|
|
2497
|
+
: '';
|
|
2498
|
+
const infoNote = f.editable && ORIGIN_INFO_NOTE[f.origin]
|
|
2499
|
+
? `<div class="feature-locked-note">` +
|
|
2500
|
+
`${escapeHtml(ORIGIN_INFO_NOTE[f.origin])}</div>`
|
|
2501
|
+
: '';
|
|
2502
|
+
info.innerHTML =
|
|
2503
|
+
`<div class="feature-name">${escapeHtml(meta.label)}</div>` +
|
|
2504
|
+
`<div class="feature-help">${escapeHtml(meta.help)}</div>` +
|
|
2505
|
+
lockedNote + infoNote;
|
|
2506
|
+
|
|
2507
|
+
const control = document.createElement('div');
|
|
2508
|
+
control.className = 'feature-control';
|
|
2509
|
+
const label = document.createElement('label');
|
|
2510
|
+
label.className = 'switch';
|
|
2511
|
+
const input = document.createElement('input');
|
|
2512
|
+
input.type = 'checkbox';
|
|
2513
|
+
input.checked = !!f.value;
|
|
2514
|
+
input.disabled = !f.editable || lockedByGate;
|
|
2515
|
+
input.addEventListener('change', () => {
|
|
2516
|
+
// Keep the cached flag value in sync (parity with the parent/master
|
|
2517
|
+
// row handlers) so a later parent flip — which re-renders from
|
|
2518
|
+
// _lastFeatureFlags — reflects this sub-row's current state rather
|
|
2519
|
+
// than a stale value.
|
|
2520
|
+
if (_lastFeatureFlags[fieldName]) {
|
|
2521
|
+
_lastFeatureFlags[fieldName] = {
|
|
2522
|
+
..._lastFeatureFlags[fieldName],
|
|
2523
|
+
value: input.checked,
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
saveFeatureFlag(fieldName, input.checked);
|
|
2527
|
+
});
|
|
2528
|
+
const slider = document.createElement('span');
|
|
2529
|
+
slider.className = 'slider';
|
|
2530
|
+
label.appendChild(input);
|
|
2531
|
+
label.appendChild(slider);
|
|
2532
|
+
control.appendChild(label);
|
|
2533
|
+
|
|
2534
|
+
row.appendChild(info);
|
|
2535
|
+
row.appendChild(control);
|
|
2536
|
+
parentEl.appendChild(row);
|
|
2420
2537
|
});
|
|
2421
2538
|
}
|
|
2422
2539
|
|
|
@@ -4190,6 +4307,11 @@ def build_settings_handlers(
|
|
|
4190
4307
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
4191
4308
|
f"Refusing to flip env-pinned tools: {', '.join(rejected)}. "
|
|
4192
4309
|
"Unset DISABLED_TOOLS / PINNED_TOOLS first.",
|
|
4310
|
+
suggestions=[
|
|
4311
|
+
"Unset the DISABLED_TOOLS / PINNED_TOOLS environment "
|
|
4312
|
+
"variables (or remove them from your addon/Docker "
|
|
4313
|
+
"config), then restart to edit these tools from the UI.",
|
|
4314
|
+
],
|
|
4193
4315
|
context={"rejected": rejected},
|
|
4194
4316
|
),
|
|
4195
4317
|
status_code=409,
|
|
@@ -4543,6 +4665,12 @@ def build_settings_handlers(
|
|
|
4543
4665
|
"enable_beta_features=true in the same save, or "
|
|
4544
4666
|
"flip the master on first."
|
|
4545
4667
|
),
|
|
4668
|
+
suggestions=[
|
|
4669
|
+
"Include enable_beta_features=true in the same save "
|
|
4670
|
+
"payload as the sub-flag(s).",
|
|
4671
|
+
"Or turn on the master 'Enable beta features' toggle "
|
|
4672
|
+
"first, then enable the sub-flag(s).",
|
|
4673
|
+
],
|
|
4546
4674
|
context={"rejected": beta_sub_writes},
|
|
4547
4675
|
),
|
|
4548
4676
|
status_code=409,
|
|
@@ -4708,6 +4836,13 @@ def build_settings_handlers(
|
|
|
4708
4836
|
create_error_response(
|
|
4709
4837
|
ErrorCode.INTERNAL_ERROR,
|
|
4710
4838
|
"Supervisor helper returned ok=False with no error",
|
|
4839
|
+
suggestions=[
|
|
4840
|
+
"Check the Home Assistant Supervisor logs and "
|
|
4841
|
+
"the add-on logs for the underlying failure.",
|
|
4842
|
+
"Report this at "
|
|
4843
|
+
"https://github.com/homeassistant-ai/ha-mcp/issues "
|
|
4844
|
+
"if it persists — this indicates an internal bug.",
|
|
4845
|
+
],
|
|
4711
4846
|
),
|
|
4712
4847
|
status_code=500,
|
|
4713
4848
|
)
|
|
@@ -5243,6 +5378,11 @@ def build_settings_handlers(
|
|
|
5243
5378
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
5244
5379
|
f"{fname!r} is set via {env_name} env var — "
|
|
5245
5380
|
"unset it to edit here.",
|
|
5381
|
+
suggestions=[
|
|
5382
|
+
f"Unset the {env_name} environment variable (or "
|
|
5383
|
+
"remove it from your addon/Docker config), then "
|
|
5384
|
+
"restart to edit this setting from the UI.",
|
|
5385
|
+
],
|
|
5246
5386
|
context={"env_var": env_name},
|
|
5247
5387
|
),
|
|
5248
5388
|
status_code=409,
|
|
@@ -5283,6 +5423,10 @@ def build_settings_handlers(
|
|
|
5283
5423
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
5284
5424
|
f"{fname!r} must be between {bounds[0]} and "
|
|
5285
5425
|
f"{bounds[1]} (got {coerced}).",
|
|
5426
|
+
suggestions=[
|
|
5427
|
+
f"Provide a value for {fname} within the range "
|
|
5428
|
+
f"{bounds[0]}–{bounds[1]}.",
|
|
5429
|
+
],
|
|
5286
5430
|
),
|
|
5287
5431
|
status_code=400,
|
|
5288
5432
|
)
|
|
@@ -5292,6 +5436,9 @@ def build_settings_handlers(
|
|
|
5292
5436
|
create_error_response(
|
|
5293
5437
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
5294
5438
|
f"{fname!r} must be one of {list(choices)} (got {coerced!r}).",
|
|
5439
|
+
suggestions=[
|
|
5440
|
+
f"Set {fname} to one of: {', '.join(map(str, choices))}.",
|
|
5441
|
+
],
|
|
5295
5442
|
),
|
|
5296
5443
|
status_code=400,
|
|
5297
5444
|
)
|
|
@@ -5342,6 +5489,11 @@ def build_settings_handlers(
|
|
|
5342
5489
|
"writes in one batch; the UI should split these "
|
|
5343
5490
|
f"into separate POSTs ({sorted(file_only)})."
|
|
5344
5491
|
),
|
|
5492
|
+
suggestions=[
|
|
5493
|
+
"Submit addon-synced fields (e.g. backup_hint, "
|
|
5494
|
+
"verify_ssl) and override-file fields in separate "
|
|
5495
|
+
"save requests.",
|
|
5496
|
+
],
|
|
5345
5497
|
),
|
|
5346
5498
|
status_code=500,
|
|
5347
5499
|
)
|
|
@@ -5367,6 +5519,13 @@ def build_settings_handlers(
|
|
|
5367
5519
|
create_error_response(
|
|
5368
5520
|
ErrorCode.INTERNAL_ERROR,
|
|
5369
5521
|
"Supervisor helper returned ok=False with no error",
|
|
5522
|
+
suggestions=[
|
|
5523
|
+
"Check the Home Assistant Supervisor logs and "
|
|
5524
|
+
"the add-on logs for the underlying failure.",
|
|
5525
|
+
"Report this at "
|
|
5526
|
+
"https://github.com/homeassistant-ai/ha-mcp/issues "
|
|
5527
|
+
"if it persists — this indicates an internal bug.",
|
|
5528
|
+
],
|
|
5370
5529
|
),
|
|
5371
5530
|
status_code=500,
|
|
5372
5531
|
)
|
|
@@ -5466,6 +5625,9 @@ def build_settings_handlers(
|
|
|
5466
5625
|
create_error_response(
|
|
5467
5626
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
5468
5627
|
f"{fname!r} expects {ftype.__name__}, got {type(raw).__name__}.",
|
|
5628
|
+
suggestions=[
|
|
5629
|
+
f"Send {fname} as a {ftype.__name__} value.",
|
|
5630
|
+
],
|
|
5469
5631
|
),
|
|
5470
5632
|
status_code=400,
|
|
5471
5633
|
)
|
|
@@ -53,7 +53,7 @@ CALLER_TOKEN_BOOTSTRAP_SERVICE = "get_caller_token"
|
|
|
53
53
|
# server-side behavior change requires it. Older components (no
|
|
54
54
|
# ``version`` in the get_caller_token response, or a version below this)
|
|
55
55
|
# get an actionable "update via HACS" error.
|
|
56
|
-
MIN_COMPONENT_VERSION = "0.5.
|
|
56
|
+
MIN_COMPONENT_VERSION = "0.5.2"
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def _version_tuple(version: str) -> tuple[int, ...]:
|
|
@@ -11,7 +11,9 @@ The tools will gracefully fail with installation instructions if the component i
|
|
|
11
11
|
Feature Flag: Set ENABLE_YAML_CONFIG_EDITING=true to enable.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
import fnmatch
|
|
14
15
|
import logging
|
|
16
|
+
import os
|
|
15
17
|
from typing import Annotated, Any
|
|
16
18
|
|
|
17
19
|
from fastmcp.exceptions import ToolError
|
|
@@ -47,6 +49,38 @@ logger = logging.getLogger(__name__)
|
|
|
47
49
|
|
|
48
50
|
_LOVELACE_DASHBOARD_PREFIX = "lovelace.dashboards."
|
|
49
51
|
|
|
52
|
+
# Maps a per-key Settings flag onto the yaml_path top-level segment it
|
|
53
|
+
# gates. Keep in lockstep with the custom component's
|
|
54
|
+
# PACKAGES_ONLY_YAML_KEYS — every key in that frozenset must appear
|
|
55
|
+
# here, otherwise a key would silently be unreachable through the
|
|
56
|
+
# wrapper. The parity invariant (keys == PACKAGES_ONLY_YAML_KEYS, and
|
|
57
|
+
# every value is a real Settings field) is enforced by
|
|
58
|
+
# test_yaml_config_tool.py::test_flag_map_matches_packages_only_keys.
|
|
59
|
+
_YAML_PACKAGES_FLAG_BY_KEY = {
|
|
60
|
+
"automation": "enable_yaml_packages_automation",
|
|
61
|
+
"script": "enable_yaml_packages_script",
|
|
62
|
+
"scene": "enable_yaml_packages_scene",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _disabled_packages_keys(settings: Any) -> list[str]:
|
|
67
|
+
"""Return the sorted list of PACKAGES_ONLY_YAML_KEYS whose Settings
|
|
68
|
+
flag is currently False.
|
|
69
|
+
|
|
70
|
+
Sorted so the value is deterministic in service payloads and test
|
|
71
|
+
assertions; the custom component treats it as a set so order is
|
|
72
|
+
irrelevant on the wire.
|
|
73
|
+
|
|
74
|
+
Uses ``getattr`` without a default so a future rename that breaks the
|
|
75
|
+
``_YAML_PACKAGES_FLAG_BY_KEY`` → ``Settings`` mapping raises loudly
|
|
76
|
+
instead of silently treating the key as disabled (a dead toggle).
|
|
77
|
+
"""
|
|
78
|
+
return sorted(
|
|
79
|
+
key
|
|
80
|
+
for key, flag in _YAML_PACKAGES_FLAG_BY_KEY.items()
|
|
81
|
+
if not getattr(settings, flag)
|
|
82
|
+
)
|
|
83
|
+
|
|
50
84
|
|
|
51
85
|
async def _check_storage_mode_dashboard_collision(client: Any, yaml_path: str) -> None:
|
|
52
86
|
"""Raise a ToolError if a storage-mode dashboard already owns the requested
|
|
@@ -237,6 +271,56 @@ class YamlConfigTools:
|
|
|
237
271
|
)
|
|
238
272
|
)
|
|
239
273
|
|
|
274
|
+
# Per-key gate: reject before the custom-component round
|
|
275
|
+
# trip when the yaml_path top-level segment matches a
|
|
276
|
+
# disabled PACKAGES_ONLY key AND the target file is under
|
|
277
|
+
# packages/. The keys (automation / script / scene) are
|
|
278
|
+
# only ACCEPTED in packages/*.yaml in the first place, so
|
|
279
|
+
# writes to configuration.yaml must fall through here and
|
|
280
|
+
# let the component-side reject with its own message that
|
|
281
|
+
# lists the storage-mode tools to use instead.
|
|
282
|
+
settings = get_global_settings()
|
|
283
|
+
disabled_keys = _disabled_packages_keys(settings)
|
|
284
|
+
top_key = yaml_path.split(".", 1)[0] if yaml_path else ""
|
|
285
|
+
# Classify the target exactly like the custom component does
|
|
286
|
+
# (os.path.normpath + fnmatch against "packages/*.yaml") so the
|
|
287
|
+
# wrapper's early reject fires for precisely the paths the
|
|
288
|
+
# component treats as a package — e.g. "./packages/x.yaml" and
|
|
289
|
+
# "packages/sub/x.yaml" both normalise/match. Any other target
|
|
290
|
+
# (configuration.yaml, a non-package path) falls through to the
|
|
291
|
+
# component, which rejects these keys with its own storage-mode-
|
|
292
|
+
# tools advisory.
|
|
293
|
+
# os.path.normpath is a pure string transform (no I/O), so the
|
|
294
|
+
# ASYNC240 blocking-call lint doesn't apply — same suppression the
|
|
295
|
+
# component uses on its identical normpath classification.
|
|
296
|
+
normalized_target = os.path.normpath(file) # noqa: ASYNC240
|
|
297
|
+
is_packages_target = fnmatch.fnmatch(normalized_target, "packages/*.yaml")
|
|
298
|
+
if is_packages_target and top_key in disabled_keys:
|
|
299
|
+
raise_tool_error(
|
|
300
|
+
create_error_response(
|
|
301
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
302
|
+
(
|
|
303
|
+
f"yaml_path key {top_key!r} is disabled. Enable "
|
|
304
|
+
f"'Allow {top_key} in packages/*.yaml' under "
|
|
305
|
+
f"YAML config editing in Server Settings to use "
|
|
306
|
+
f"this key, or use the storage-mode tool "
|
|
307
|
+
f"(ha_config_set_{top_key})."
|
|
308
|
+
),
|
|
309
|
+
suggestions=[
|
|
310
|
+
f"Enable 'Allow {top_key} in packages/*.yaml' under "
|
|
311
|
+
"YAML config editing in the ha-mcp Server Settings "
|
|
312
|
+
"panel, then retry.",
|
|
313
|
+
f"Or use the storage-mode tool ha_config_set_{top_key} "
|
|
314
|
+
"instead of editing packages YAML directly.",
|
|
315
|
+
],
|
|
316
|
+
context={
|
|
317
|
+
"yaml_path": yaml_path,
|
|
318
|
+
"disabled_key": top_key,
|
|
319
|
+
"file": file,
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
|
|
240
324
|
# Storage-mode dashboard collision check (only for lovelace.dashboards.*).
|
|
241
325
|
# Skip on `remove` so users can clean up YAML entries that conflict
|
|
242
326
|
# with a storage-mode dashboard (e.g., during a migration).
|
|
@@ -252,6 +336,7 @@ class YamlConfigTools:
|
|
|
252
336
|
"action": action,
|
|
253
337
|
"yaml_path": yaml_path,
|
|
254
338
|
"backup": backup,
|
|
339
|
+
"disabled_packages_keys": disabled_keys,
|
|
255
340
|
}
|
|
256
341
|
if content is not None:
|
|
257
342
|
service_data["content"] = content
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""FastMCP middleware that converts Pydantic validation errors to structured ToolErrors.
|
|
2
|
+
|
|
3
|
+
When a model passes the wrong type for a tool parameter (e.g. a JSON string where
|
|
4
|
+
a dict is required), FastMCP raises a PydanticValidationError with a raw message
|
|
5
|
+
like "Input should be a valid dictionary". This middleware intercepts those errors
|
|
6
|
+
and converts them to ha-mcp's structured format with actionable guidance.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
|
|
15
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
16
|
+
|
|
17
|
+
from ..errors import create_validation_error
|
|
18
|
+
from .helpers import raise_tool_error
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Maps Pydantic error types to model-readable fix hints.
|
|
23
|
+
# FastMCP uses non-strict Pydantic: scalar mismatches (bool, int) are coerced
|
|
24
|
+
# rather than rejected, so only dict_type and list_type fire in practice.
|
|
25
|
+
_TYPE_HINTS: dict[str, str] = {
|
|
26
|
+
"dict_type": (
|
|
27
|
+
"expected a JSON object. "
|
|
28
|
+
'Pass {"key": "value"} directly, not a JSON-encoded string.'
|
|
29
|
+
),
|
|
30
|
+
"list_type": (
|
|
31
|
+
"expected a JSON array. Pass [...] directly, not a JSON-encoded string."
|
|
32
|
+
),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ValidationErrorMiddleware(Middleware):
|
|
37
|
+
"""Convert PydanticValidationError from argument validation into ToolErrors."""
|
|
38
|
+
|
|
39
|
+
async def on_call_tool(
|
|
40
|
+
self, context: MiddlewareContext, call_next: CallNext
|
|
41
|
+
) -> Any:
|
|
42
|
+
try:
|
|
43
|
+
return await call_next(context)
|
|
44
|
+
except PydanticValidationError as exc:
|
|
45
|
+
errors = exc.errors(include_url=False)
|
|
46
|
+
parts: list[str] = []
|
|
47
|
+
for err in errors:
|
|
48
|
+
loc = err.get("loc", ())
|
|
49
|
+
param = ".".join(str(p) for p in loc if p != "__root__")
|
|
50
|
+
hint = _TYPE_HINTS.get(err["type"], err["msg"])
|
|
51
|
+
parts.append(f"`{param}`: {hint}" if param else hint)
|
|
52
|
+
raise_tool_error(
|
|
53
|
+
create_validation_error(
|
|
54
|
+
"; ".join(parts) if parts else "Invalid argument types.",
|
|
55
|
+
details=", ".join(err["type"] for err in errors),
|
|
56
|
+
)
|
|
57
|
+
)
|
|
@@ -99,6 +99,7 @@ src/ha_mcp/tools/tools_voice_assistant.py
|
|
|
99
99
|
src/ha_mcp/tools/tools_yaml_config.py
|
|
100
100
|
src/ha_mcp/tools/tools_zones.py
|
|
101
101
|
src/ha_mcp/tools/util_helpers.py
|
|
102
|
+
src/ha_mcp/tools/validation_middleware.py
|
|
102
103
|
src/ha_mcp/transforms/__init__.py
|
|
103
104
|
src/ha_mcp/transforms/categorized_search.py
|
|
104
105
|
src/ha_mcp/transforms/lite_docstrings.py
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/transforms/lite_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp/utils/kill_signal_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev622 → ha_mcp_dev-7.6.0.dev624}/src/ha_mcp_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|