ha-mcp-dev 7.5.0.dev525__tar.gz → 7.5.0.dev527__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.5.0.dev525/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev527}/PKG-INFO +4 -4
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/README.md +3 -3
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/pyproject.toml +1 -1
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/helpers.py +84 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_addons.py +19 -4
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_areas.py +35 -23
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_calendar.py +13 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_categories.py +35 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_config_automations.py +10 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_config_helpers.py +76 -9
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_config_scripts.py +10 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_energy.py +24 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_entities.py +17 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_groups.py +28 -1
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_hacs.py +16 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_integrations.py +29 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_labels.py +35 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_registry.py +24 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_resources.py +24 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_todo.py +31 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_zones.py +22 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527/src/ha_mcp_dev.egg-info}/PKG-INFO +4 -4
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/setup.cfg +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/tests/test_env_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ha-mcp-dev
|
|
3
|
-
Version: 7.5.0.
|
|
3
|
+
Version: 7.5.0.dev527
|
|
4
4
|
Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
|
|
5
5
|
Author-email: Julien <github@qc-h.net>
|
|
6
6
|
License: MIT
|
|
@@ -38,7 +38,7 @@ Dynamic: license-file
|
|
|
38
38
|
<!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
|
|
39
39
|
|
|
40
40
|
<p align="center">
|
|
41
|
-
<img src="https://img.shields.io/badge/tools-
|
|
41
|
+
<img src="https://img.shields.io/badge/tools-88-blue" alt="95+ Tools">
|
|
42
42
|
<a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
|
|
43
43
|
<a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
|
|
44
44
|
<a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
|
|
@@ -181,7 +181,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
181
181
|
<details>
|
|
182
182
|
<!-- TOOLS_TABLE_START -->
|
|
183
183
|
|
|
184
|
-
<summary><b>Complete Tool List (
|
|
184
|
+
<summary><b>Complete Tool List (88 tools)</b></summary>
|
|
185
185
|
|
|
186
186
|
| Category | Tools |
|
|
187
187
|
|----------|-------|
|
|
@@ -198,7 +198,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
198
198
|
| **Files** | `ha_delete_file` *(beta)*, `ha_list_files` *(beta)*, `ha_read_file` *(beta)*, `ha_write_file` *(beta)* |
|
|
199
199
|
| **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
|
|
200
200
|
| **HACS** | `ha_hacs_add_repository`, `ha_hacs_download`, `ha_hacs_repository_info`, `ha_hacs_search` |
|
|
201
|
-
| **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_delete_helpers_integrations
|
|
201
|
+
| **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_delete_helpers_integrations` |
|
|
202
202
|
| **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
|
|
203
203
|
| **Integrations** | `ha_get_integration`, `ha_set_integration_enabled` |
|
|
204
204
|
| **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<img src="https://img.shields.io/badge/tools-
|
|
11
|
+
<img src="https://img.shields.io/badge/tools-88-blue" alt="95+ Tools">
|
|
12
12
|
<a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
|
|
13
13
|
<a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
|
|
14
14
|
<a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
|
|
@@ -151,7 +151,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
151
151
|
<details>
|
|
152
152
|
<!-- TOOLS_TABLE_START -->
|
|
153
153
|
|
|
154
|
-
<summary><b>Complete Tool List (
|
|
154
|
+
<summary><b>Complete Tool List (88 tools)</b></summary>
|
|
155
155
|
|
|
156
156
|
| Category | Tools |
|
|
157
157
|
|----------|-------|
|
|
@@ -168,7 +168,7 @@ Spend less time configuring, more time enjoying your smart home.
|
|
|
168
168
|
| **Files** | `ha_delete_file` *(beta)*, `ha_list_files` *(beta)*, `ha_read_file` *(beta)*, `ha_write_file` *(beta)* |
|
|
169
169
|
| **Groups** | `ha_config_list_groups`, `ha_config_remove_group`, `ha_config_set_group` |
|
|
170
170
|
| **HACS** | `ha_hacs_add_repository`, `ha_hacs_download`, `ha_hacs_repository_info`, `ha_hacs_search` |
|
|
171
|
-
| **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_delete_helpers_integrations
|
|
171
|
+
| **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_delete_helpers_integrations` |
|
|
172
172
|
| **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
|
|
173
173
|
| **Integrations** | `ha_get_integration`, `ha_set_integration_enabled` |
|
|
174
174
|
| **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.5.0.
|
|
7
|
+
version = "7.5.0.dev527"
|
|
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"
|
|
@@ -78,6 +78,90 @@ def extract_tool_error_message(te: ToolError) -> str:
|
|
|
78
78
|
return str(te)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def validate_identifier_not_empty(
|
|
82
|
+
value: str | None,
|
|
83
|
+
param_name: str,
|
|
84
|
+
*,
|
|
85
|
+
message: str | None = None,
|
|
86
|
+
suggestions: list[str] | None = None,
|
|
87
|
+
context: dict[str, Any] | None = None,
|
|
88
|
+
) -> str:
|
|
89
|
+
"""Reject ``None``, empty, or whitespace-only identifier values.
|
|
90
|
+
|
|
91
|
+
Surfaces a structured ``VALIDATION_INVALID_PARAMETER`` response so
|
|
92
|
+
CRUD-style tools fail loudly on missing identifiers instead of silently
|
|
93
|
+
routing on Python falsy semantics or relying on Home Assistant to
|
|
94
|
+
translate a missing identifier into a generic ``RESOURCE_NOT_FOUND``.
|
|
95
|
+
|
|
96
|
+
The destructive class this protects against:
|
|
97
|
+
``action = "update" if label_id else "create"`` — passing ``""`` as
|
|
98
|
+
``label_id`` silently routes to ``create`` when the caller intended
|
|
99
|
+
``update``. The whitespace class this protects against: ``" "`` is truthy
|
|
100
|
+
in Python so ``if not value:`` lets it through, but Home Assistant has
|
|
101
|
+
no entry with id ``" "``.
|
|
102
|
+
|
|
103
|
+
The value is checked but not normalised: ``" abc "`` (a real id wrapped
|
|
104
|
+
in spaces) is accepted as-is and returned untouched. Only purely empty or
|
|
105
|
+
purely whitespace strings are rejected.
|
|
106
|
+
|
|
107
|
+
``None`` is also rejected by this helper. Callers for whom ``None`` is a
|
|
108
|
+
documented sentinel (e.g. ``label_id=None`` meaning "list all" or
|
|
109
|
+
"create new") must gate the call themselves with
|
|
110
|
+
``if value is not None: validate_identifier_not_empty(value, ...)``.
|
|
111
|
+
|
|
112
|
+
Returning ``str`` (rather than ``None``) lets call sites use the helper
|
|
113
|
+
in narrowing position — ``name = validate_identifier_not_empty(name, …)``
|
|
114
|
+
re-binds ``name`` from ``str | None`` to ``str`` so mypy can prove later
|
|
115
|
+
uses are safe without a duplicate inline check.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
value: Identifier string supplied by the caller. ``None`` is
|
|
119
|
+
rejected — see the ``None``-sentinel note above for the caller
|
|
120
|
+
pattern that permits the sentinel.
|
|
121
|
+
param_name: Name of the parameter, used in the structured error
|
|
122
|
+
response's ``context`` and the human-readable message.
|
|
123
|
+
message: Optional override for the human-readable error message.
|
|
124
|
+
Defaults to ``"{param_name} must be a non-empty, non-whitespace
|
|
125
|
+
string"`` when omitted.
|
|
126
|
+
suggestions: Optional list of guidance strings for the response's
|
|
127
|
+
``suggestions`` field. Defaults to a generic
|
|
128
|
+
"provide a non-empty value" hint.
|
|
129
|
+
context: Optional additional context fields merged into the error
|
|
130
|
+
response (e.g. ``{"action": "update"}``). The keys ``parameter``
|
|
131
|
+
and ``value`` are always set and take precedence.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The validated ``value`` unchanged (typed as ``str``).
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ToolError: When ``value`` is ``None``, empty, or whitespace-only —
|
|
138
|
+
carrying a structured ``VALIDATION_INVALID_PARAMETER`` response
|
|
139
|
+
with the parameter name, the offending value (for diagnostics),
|
|
140
|
+
and the suggestions.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
>>> validate_identifier_not_empty(label_id, "label_id",
|
|
144
|
+
... suggestions=["Omit label_id to create a new label"])
|
|
145
|
+
"""
|
|
146
|
+
if value is not None and value.strip():
|
|
147
|
+
return value
|
|
148
|
+
|
|
149
|
+
final_context: dict[str, Any] = {}
|
|
150
|
+
if context:
|
|
151
|
+
final_context.update(context)
|
|
152
|
+
final_context["parameter"] = param_name
|
|
153
|
+
final_context["value"] = value
|
|
154
|
+
raise_tool_error(
|
|
155
|
+
create_error_response(
|
|
156
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
157
|
+
message or f"{param_name} must be a non-empty, non-whitespace string",
|
|
158
|
+
suggestions=suggestions
|
|
159
|
+
or [f"Provide a valid non-empty value for {param_name}"],
|
|
160
|
+
context=final_context,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
81
165
|
async def get_connected_ws_client(
|
|
82
166
|
base_url: str, token: str, verify_ssl: bool | None = None
|
|
83
167
|
) -> tuple[HomeAssistantWebSocketClient | None, dict[str, Any] | None]:
|
|
@@ -38,6 +38,7 @@ from .helpers import (
|
|
|
38
38
|
get_connected_ws_client,
|
|
39
39
|
log_tool_usage,
|
|
40
40
|
raise_tool_error,
|
|
41
|
+
validate_identifier_not_empty,
|
|
41
42
|
)
|
|
42
43
|
from .util_helpers import ANSI_ESCAPE_RE
|
|
43
44
|
|
|
@@ -1208,14 +1209,16 @@ def _apply_array_ops(
|
|
|
1208
1209
|
)
|
|
1209
1210
|
)
|
|
1210
1211
|
new_id = new_item[id_field]
|
|
1211
|
-
if new_id is None or new_id
|
|
1212
|
+
if new_id is None or (isinstance(new_id, str) and not new_id.strip()):
|
|
1212
1213
|
# Items missing the id field have `dict.get(id_field) == None`
|
|
1213
|
-
# by default, so allowing None/"" ids would
|
|
1214
|
-
# delete ops match unrelated items.
|
|
1214
|
+
# by default, so allowing None/""/whitespace-only ids would
|
|
1215
|
+
# let later patch / delete ops match unrelated items.
|
|
1216
|
+
# Non-string ids (e.g. integers) stay valid by design — see
|
|
1217
|
+
# ``test_add_with_integer_zero_id_is_accepted``.
|
|
1215
1218
|
raise_tool_error(
|
|
1216
1219
|
create_validation_error(
|
|
1217
1220
|
f"array_patch add op #{index} item {id_field!r} cannot be "
|
|
1218
|
-
"None or
|
|
1221
|
+
"None, empty, or whitespace-only",
|
|
1219
1222
|
parameter=f"array_patch.operations[{index}].item.{id_field}",
|
|
1220
1223
|
)
|
|
1221
1224
|
)
|
|
@@ -1954,6 +1957,18 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
1954
1957
|
ha_manage_addon(slug="...", path="/api/state",
|
|
1955
1958
|
request_headers={"Accept": "text/plain"})
|
|
1956
1959
|
"""
|
|
1960
|
+
# Empty/whitespace slug would propagate to every dispatch arm
|
|
1961
|
+
# (Supervisor API, ingress proxy, websocket bridge) and surface as a
|
|
1962
|
+
# misleading "addon not found" or 404 from the Supervisor. Reject
|
|
1963
|
+
# up-front so the caller learns the slug was unusable before any
|
|
1964
|
+
# backend call.
|
|
1965
|
+
validate_identifier_not_empty(
|
|
1966
|
+
slug,
|
|
1967
|
+
"slug",
|
|
1968
|
+
suggestions=[
|
|
1969
|
+
"Use ha_get_addon() to discover installed add-on slugs",
|
|
1970
|
+
],
|
|
1971
|
+
)
|
|
1957
1972
|
# Build config payload from provided config parameters
|
|
1958
1973
|
config_data: dict[str, Any] = {}
|
|
1959
1974
|
if options:
|
|
@@ -18,6 +18,7 @@ from .helpers import (
|
|
|
18
18
|
log_tool_usage,
|
|
19
19
|
raise_tool_error,
|
|
20
20
|
register_tool_methods,
|
|
21
|
+
validate_identifier_not_empty,
|
|
21
22
|
)
|
|
22
23
|
from .util_helpers import parse_string_list_param
|
|
23
24
|
|
|
@@ -465,19 +466,19 @@ class AreaTools:
|
|
|
465
466
|
],
|
|
466
467
|
))
|
|
467
468
|
|
|
468
|
-
#
|
|
469
|
-
#
|
|
470
|
-
#
|
|
471
|
-
if id
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
"id
|
|
475
|
-
context={"kind": kind},
|
|
469
|
+
# ``None`` stays the documented "create-new" sentinel; explicit
|
|
470
|
+
# empty/whitespace would silently route to the ``if id:`` create
|
|
471
|
+
# branch below and lose update intent.
|
|
472
|
+
if id is not None:
|
|
473
|
+
validate_identifier_not_empty(
|
|
474
|
+
id,
|
|
475
|
+
"id",
|
|
476
476
|
suggestions=[
|
|
477
477
|
"Omit id entirely to create a new entry",
|
|
478
478
|
"Pass a real area_id/floor_id to update an existing entry",
|
|
479
479
|
],
|
|
480
|
-
|
|
480
|
+
context={"kind": kind},
|
|
481
|
+
)
|
|
481
482
|
|
|
482
483
|
if kind == "area":
|
|
483
484
|
if id:
|
|
@@ -486,13 +487,15 @@ class AreaTools:
|
|
|
486
487
|
)
|
|
487
488
|
operation = "update"
|
|
488
489
|
else:
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
490
|
+
# Reassignment narrows ``name`` from ``str | None`` to
|
|
491
|
+
# ``str`` for the build-message call below.
|
|
492
|
+
name = validate_identifier_not_empty(
|
|
493
|
+
name,
|
|
494
|
+
"name",
|
|
495
|
+
message="name is required when creating a new area",
|
|
496
|
+
context={"operation": "create_area"},
|
|
497
|
+
suggestions=["Provide a non-empty name for the new area"],
|
|
498
|
+
)
|
|
496
499
|
message = self._build_area_create_message(
|
|
497
500
|
name, floor_id, icon, parsed_aliases, picture,
|
|
498
501
|
)
|
|
@@ -506,13 +509,13 @@ class AreaTools:
|
|
|
506
509
|
)
|
|
507
510
|
operation = "update"
|
|
508
511
|
else:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
512
|
+
name = validate_identifier_not_empty(
|
|
513
|
+
name,
|
|
514
|
+
"name",
|
|
515
|
+
message="name is required when creating a new floor",
|
|
516
|
+
context={"operation": "create_floor"},
|
|
517
|
+
suggestions=["Provide a non-empty name for the new floor"],
|
|
518
|
+
)
|
|
516
519
|
message = self._build_floor_create_message(
|
|
517
520
|
name, level, icon, parsed_aliases,
|
|
518
521
|
)
|
|
@@ -590,6 +593,15 @@ class AreaTools:
|
|
|
590
593
|
registry = "area_registry" if kind == "area" else "floor_registry"
|
|
591
594
|
id_key = "area_id" if kind == "area" else "floor_id"
|
|
592
595
|
try:
|
|
596
|
+
# Empty/whitespace would surface as a misleading HA delete-failure.
|
|
597
|
+
validate_identifier_not_empty(
|
|
598
|
+
id,
|
|
599
|
+
"id",
|
|
600
|
+
suggestions=[
|
|
601
|
+
f"Pass a valid {id_key} (use ha_list_floors_areas() to list)",
|
|
602
|
+
],
|
|
603
|
+
context={"action": "remove", "kind": kind},
|
|
604
|
+
)
|
|
593
605
|
message: dict[str, Any] = {
|
|
594
606
|
"type": f"config/{registry}/delete",
|
|
595
607
|
id_key: id,
|
|
@@ -21,6 +21,7 @@ from .helpers import (
|
|
|
21
21
|
log_tool_usage,
|
|
22
22
|
raise_tool_error,
|
|
23
23
|
register_tool_methods,
|
|
24
|
+
validate_identifier_not_empty,
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
@@ -354,6 +355,18 @@ class CalendarTools:
|
|
|
354
355
|
],
|
|
355
356
|
))
|
|
356
357
|
|
|
358
|
+
# entity_id format-check above does not cover the ``uid`` parameter.
|
|
359
|
+
# Empty/whitespace uid would flow through to ``calendar.delete_event``
|
|
360
|
+
# and HA returns a misleading "event not found".
|
|
361
|
+
validate_identifier_not_empty(
|
|
362
|
+
uid,
|
|
363
|
+
"uid",
|
|
364
|
+
suggestions=[
|
|
365
|
+
"Use ha_config_get_calendar_events() to list events and obtain valid UIDs",
|
|
366
|
+
],
|
|
367
|
+
context={"entity_id": entity_id},
|
|
368
|
+
)
|
|
369
|
+
|
|
357
370
|
# Build service data
|
|
358
371
|
service_data: dict[str, Any] = {
|
|
359
372
|
"entity_id": entity_id,
|
|
@@ -21,6 +21,7 @@ from .helpers import (
|
|
|
21
21
|
log_tool_usage,
|
|
22
22
|
raise_tool_error,
|
|
23
23
|
register_tool_methods,
|
|
24
|
+
validate_identifier_not_empty,
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
@@ -76,6 +77,18 @@ class CategoryTools:
|
|
|
76
77
|
Use ha_set_entity(categories={"automation": "category_id"}) to assign categories to entities.
|
|
77
78
|
"""
|
|
78
79
|
try:
|
|
80
|
+
# ``None`` stays the documented "list-all" sentinel; explicit
|
|
81
|
+
# empty/whitespace is rejected by ``validate_identifier_not_empty``.
|
|
82
|
+
if category_id is not None:
|
|
83
|
+
validate_identifier_not_empty(
|
|
84
|
+
category_id,
|
|
85
|
+
"category_id",
|
|
86
|
+
suggestions=[
|
|
87
|
+
"Omit category_id to list all categories for the scope",
|
|
88
|
+
"Pass a valid non-empty category_id",
|
|
89
|
+
],
|
|
90
|
+
context={"action": "get", "scope": scope},
|
|
91
|
+
)
|
|
79
92
|
message: dict[str, Any] = {
|
|
80
93
|
"type": "config/category_registry/list",
|
|
81
94
|
"scope": scope,
|
|
@@ -194,6 +207,19 @@ class CategoryTools:
|
|
|
194
207
|
After creating a category, use ha_set_entity(categories={"automation": "category_id"}) to assign it.
|
|
195
208
|
"""
|
|
196
209
|
try:
|
|
210
|
+
# ``None`` stays the documented "create-new" sentinel; explicit
|
|
211
|
+
# empty/whitespace is rejected so the create/update discriminator
|
|
212
|
+
# below cannot silently route an intended update to create.
|
|
213
|
+
if category_id is not None:
|
|
214
|
+
validate_identifier_not_empty(
|
|
215
|
+
category_id,
|
|
216
|
+
"category_id",
|
|
217
|
+
suggestions=[
|
|
218
|
+
"Omit category_id entirely to create a new category",
|
|
219
|
+
"Pass a valid category_id to update an existing category",
|
|
220
|
+
],
|
|
221
|
+
context={"action": "set", "scope": scope, "name": name},
|
|
222
|
+
)
|
|
197
223
|
action = "update" if category_id else "create"
|
|
198
224
|
|
|
199
225
|
message: dict[str, Any] = {
|
|
@@ -282,6 +308,15 @@ class CategoryTools:
|
|
|
282
308
|
This action cannot be undone.
|
|
283
309
|
"""
|
|
284
310
|
try:
|
|
311
|
+
# Empty/whitespace would surface as a misleading HA delete-failure.
|
|
312
|
+
validate_identifier_not_empty(
|
|
313
|
+
category_id,
|
|
314
|
+
"category_id",
|
|
315
|
+
suggestions=[
|
|
316
|
+
"Pass a valid category_id (use ha_config_get_category() to list)",
|
|
317
|
+
],
|
|
318
|
+
context={"action": "remove", "scope": scope},
|
|
319
|
+
)
|
|
285
320
|
message: dict[str, Any] = {
|
|
286
321
|
"type": "config/category_registry/delete",
|
|
287
322
|
"scope": scope,
|
{ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -33,6 +33,7 @@ from .helpers import (
|
|
|
33
33
|
log_tool_usage,
|
|
34
34
|
raise_tool_error,
|
|
35
35
|
register_tool_methods,
|
|
36
|
+
validate_identifier_not_empty,
|
|
36
37
|
)
|
|
37
38
|
from .reference_validator import validate_config_references
|
|
38
39
|
from .util_helpers import (
|
|
@@ -965,6 +966,15 @@ class AutomationConfigTools:
|
|
|
965
966
|
**WARNING:** Deleting an automation removes it permanently from your Home Assistant configuration.
|
|
966
967
|
"""
|
|
967
968
|
try:
|
|
969
|
+
# Empty/whitespace would surface as a misleading HA delete-failure.
|
|
970
|
+
validate_identifier_not_empty(
|
|
971
|
+
identifier,
|
|
972
|
+
"identifier",
|
|
973
|
+
suggestions=[
|
|
974
|
+
"Use ha_search_entities(domain_filter='automation') to find existing automations"
|
|
975
|
+
],
|
|
976
|
+
context={"operation": "remove_automation"},
|
|
977
|
+
)
|
|
968
978
|
# Resolve entity_id for wait verification (identifier may be a unique_id)
|
|
969
979
|
entity_id_for_wait = await self._resolve_automation_entity_id(identifier)
|
|
970
980
|
if not entity_id_for_wait:
|
{ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
@@ -16,7 +16,12 @@ from pydantic import AliasChoices, Field
|
|
|
16
16
|
|
|
17
17
|
from ..client.rest_client import HomeAssistantAPIError
|
|
18
18
|
from ..errors import ErrorCode, create_error_response
|
|
19
|
-
from .helpers import
|
|
19
|
+
from .helpers import (
|
|
20
|
+
exception_to_structured_error,
|
|
21
|
+
log_tool_usage,
|
|
22
|
+
raise_tool_error,
|
|
23
|
+
validate_identifier_not_empty,
|
|
24
|
+
)
|
|
20
25
|
from .tools_config_entry_flow import (
|
|
21
26
|
FLOW_HELPER_TYPES,
|
|
22
27
|
create_flow_helper,
|
|
@@ -1223,11 +1228,12 @@ async def _check_name_collision(
|
|
|
1223
1228
|
duplicate entity instead of updating the original. Detect and reject before
|
|
1224
1229
|
we send the create message, pointing the caller at the existing helper_id.
|
|
1225
1230
|
|
|
1226
|
-
Empty / missing `name` is left to the existing
|
|
1227
|
-
so the user sees the standard "name is
|
|
1228
|
-
spurious collision miss
|
|
1231
|
+
Empty / missing / whitespace-only `name` is left to the existing
|
|
1232
|
+
name-required check downstream so the user sees the standard "name is
|
|
1233
|
+
required" error rather than a spurious collision miss (or a wasted WS
|
|
1234
|
+
round-trip on a name the validator is about to reject anyway).
|
|
1229
1235
|
"""
|
|
1230
|
-
if not name:
|
|
1236
|
+
if not name or not name.strip():
|
|
1231
1237
|
return
|
|
1232
1238
|
|
|
1233
1239
|
target_slug = _slugify_helper_name(name)
|
|
@@ -1563,6 +1569,22 @@ async def _handle_flow_helper(
|
|
|
1563
1569
|
is consistent has already happened upstream in ha_config_set_helper.
|
|
1564
1570
|
"""
|
|
1565
1571
|
if action is None:
|
|
1572
|
+
# Defence in depth: when reached via the legacy implicit-action
|
|
1573
|
+
# path, an empty/whitespace ``helper_id`` would otherwise be falsy
|
|
1574
|
+
# and silently route to ``create`` — same destructive intent-loss
|
|
1575
|
+
# class as the registry-metadata twins. ``None`` stays the
|
|
1576
|
+
# documented "create-new" sentinel.
|
|
1577
|
+
if helper_id is not None:
|
|
1578
|
+
validate_identifier_not_empty(
|
|
1579
|
+
helper_id,
|
|
1580
|
+
"helper_id",
|
|
1581
|
+
suggestions=[
|
|
1582
|
+
"Omit helper_id entirely to create a new flow helper",
|
|
1583
|
+
"Pass a valid helper_id to update an existing one",
|
|
1584
|
+
"Or pass action='create' / action='update' explicitly",
|
|
1585
|
+
],
|
|
1586
|
+
context={"helper_type": helper_type},
|
|
1587
|
+
)
|
|
1566
1588
|
action = "update" if helper_id else "create"
|
|
1567
1589
|
|
|
1568
1590
|
# Normalize empty string to None, matching ha_config_set_helper's treatment
|
|
@@ -1618,7 +1640,7 @@ async def _handle_flow_helper(
|
|
|
1618
1640
|
# caller-supplied `name` (you cannot rename a flow helper through its
|
|
1619
1641
|
# options flow). Strip `name` from config_dict and emit a warning so the
|
|
1620
1642
|
# caller learns their attempted rename was a no-op.
|
|
1621
|
-
if action == "create" and name and "name" not in config_dict:
|
|
1643
|
+
if action == "create" and name and name.strip() and "name" not in config_dict:
|
|
1622
1644
|
schema_fields = await get_user_step_field_names(client, helper_type)
|
|
1623
1645
|
if schema_fields is None or "name" in schema_fields:
|
|
1624
1646
|
config_dict["name"] = name
|
|
@@ -1661,7 +1683,12 @@ async def _handle_flow_helper(
|
|
|
1661
1683
|
# Some helpers (switch_as_x) deliberately don't have `name` injected into
|
|
1662
1684
|
# config_dict because their schema rejects it — but the tool still
|
|
1663
1685
|
# requires `name` to be supplied so callers fail fast and consistently.
|
|
1664
|
-
|
|
1686
|
+
# Reject whitespace-only too (``" "`` is truthy in Python) so the
|
|
1687
|
+
# flow-helper create gate is coherent with the simple-helper twin.
|
|
1688
|
+
config_name = config_dict.get("name")
|
|
1689
|
+
config_name_ok = isinstance(config_name, str) and bool(config_name.strip())
|
|
1690
|
+
name_ok = name is not None and bool(name.strip())
|
|
1691
|
+
if not (name_ok or config_name_ok):
|
|
1665
1692
|
raise_tool_error(
|
|
1666
1693
|
create_error_response(
|
|
1667
1694
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
@@ -2373,9 +2400,45 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
2373
2400
|
],
|
|
2374
2401
|
)
|
|
2375
2402
|
)
|
|
2403
|
+
# Explicit-action update path: helper_id was confirmed non-None
|
|
2404
|
+
# above; close the whitespace gap before the FLOW dispatch
|
|
2405
|
+
# below (``if helper_type in FLOW_HELPER_TYPES: return await
|
|
2406
|
+
# _handle_flow_helper(...)``). The simple-path whitespace twin
|
|
2407
|
+
# inside the ``elif action == "update":`` branch fires after
|
|
2408
|
+
# the FLOW dispatch returns and so is unreachable for flow
|
|
2409
|
+
# helpers; without this guard, ``helper_id=" "`` would reach
|
|
2410
|
+
# ``update_flow_helper`` and surface as a misleading "entry
|
|
2411
|
+
# not found" from HA.
|
|
2412
|
+
if action == "update" and helper_id is not None:
|
|
2413
|
+
validate_identifier_not_empty(
|
|
2414
|
+
helper_id,
|
|
2415
|
+
"helper_id",
|
|
2416
|
+
suggestions=[
|
|
2417
|
+
"Pass a valid helper_id to identify the helper to update",
|
|
2418
|
+
"Or omit helper_id and pass action='create' to create a new helper",
|
|
2419
|
+
],
|
|
2420
|
+
context={"helper_type": helper_type, "action": action},
|
|
2421
|
+
)
|
|
2376
2422
|
else:
|
|
2377
2423
|
# Implicit discriminator (back-compat). Pass action='create'
|
|
2378
2424
|
# or action='update' explicitly to avoid the inference.
|
|
2425
|
+
# Reject empty/whitespace helper_id up front: ``bool("")`` is
|
|
2426
|
+
# False, so without this gate ``helper_id=""`` silently routes
|
|
2427
|
+
# to ``create`` even when the caller's intent was ``update``,
|
|
2428
|
+
# producing a destructive intent-loss class identical to the
|
|
2429
|
+
# one closed for labels/categories. ``None`` stays the
|
|
2430
|
+
# documented "create-new" sentinel.
|
|
2431
|
+
if helper_id is not None:
|
|
2432
|
+
validate_identifier_not_empty(
|
|
2433
|
+
helper_id,
|
|
2434
|
+
"helper_id",
|
|
2435
|
+
suggestions=[
|
|
2436
|
+
"Omit helper_id entirely to create a new helper",
|
|
2437
|
+
"Pass a valid helper_id to update an existing helper",
|
|
2438
|
+
"Or pass action='create' / action='update' explicitly to declare intent",
|
|
2439
|
+
],
|
|
2440
|
+
context={"helper_type": helper_type},
|
|
2441
|
+
)
|
|
2379
2442
|
action = "update" if helper_id else "create"
|
|
2380
2443
|
|
|
2381
2444
|
# Bug 4b/7c/10/14 (issue #1150): reject typed params that don't apply
|
|
@@ -2496,7 +2559,9 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
2496
2559
|
)
|
|
2497
2560
|
|
|
2498
2561
|
if action == "create":
|
|
2499
|
-
if not name
|
|
2562
|
+
# ``bool(" ")`` is True, so plain ``if not name`` would let
|
|
2563
|
+
# whitespace-only names through to a downstream HA error.
|
|
2564
|
+
if not name or not name.strip():
|
|
2500
2565
|
raise_tool_error(
|
|
2501
2566
|
create_error_response(
|
|
2502
2567
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
@@ -2839,7 +2904,9 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
2839
2904
|
)
|
|
2840
2905
|
|
|
2841
2906
|
elif action == "update":
|
|
2842
|
-
|
|
2907
|
+
# Same whitespace-upgrade rationale as the create-name check
|
|
2908
|
+
# above — ``if not helper_id`` would let ``" "`` through.
|
|
2909
|
+
if not helper_id or not helper_id.strip():
|
|
2843
2910
|
raise_tool_error(
|
|
2844
2911
|
create_error_response(
|
|
2845
2912
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
{ha_mcp_dev-7.5.0.dev525 → ha_mcp_dev-7.5.0.dev527}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
@@ -28,6 +28,7 @@ from .helpers import (
|
|
|
28
28
|
log_tool_usage,
|
|
29
29
|
raise_tool_error,
|
|
30
30
|
register_tool_methods,
|
|
31
|
+
validate_identifier_not_empty,
|
|
31
32
|
)
|
|
32
33
|
from .reference_validator import validate_config_references
|
|
33
34
|
from .util_helpers import (
|
|
@@ -619,6 +620,15 @@ class ConfigScriptTools:
|
|
|
619
620
|
**WARNING:** Deleting a script that is used by automations may cause those automations to fail.
|
|
620
621
|
"""
|
|
621
622
|
try:
|
|
623
|
+
# Empty/whitespace would surface as a misleading HA delete-failure.
|
|
624
|
+
validate_identifier_not_empty(
|
|
625
|
+
script_id,
|
|
626
|
+
"script_id",
|
|
627
|
+
suggestions=[
|
|
628
|
+
"Use ha_search_entities(domain_filter='script') to find existing script_ids"
|
|
629
|
+
],
|
|
630
|
+
context={"operation": "remove_script"},
|
|
631
|
+
)
|
|
622
632
|
result = await self._client.delete_script_config(script_id)
|
|
623
633
|
|
|
624
634
|
# Wait for script to be removed
|
|
@@ -41,6 +41,7 @@ from .helpers import (
|
|
|
41
41
|
log_tool_usage,
|
|
42
42
|
raise_tool_error,
|
|
43
43
|
register_tool_methods,
|
|
44
|
+
validate_identifier_not_empty,
|
|
44
45
|
)
|
|
45
46
|
|
|
46
47
|
logger = logging.getLogger(__name__)
|
|
@@ -1011,6 +1012,18 @@ class EnergyTools:
|
|
|
1011
1012
|
],
|
|
1012
1013
|
)
|
|
1013
1014
|
)
|
|
1015
|
+
# Empty/whitespace stat_consumption would write a ``{"stat_consumption": ""}``
|
|
1016
|
+
# entry to energy prefs storage — a phantom row keyed on an empty
|
|
1017
|
+
# sensor reference. Same multi-modal-destructive class as
|
|
1018
|
+
# ``ha_manage_addon`` slug guard.
|
|
1019
|
+
validate_identifier_not_empty(
|
|
1020
|
+
stat_consumption,
|
|
1021
|
+
"stat_consumption",
|
|
1022
|
+
suggestions=[
|
|
1023
|
+
"Pass stat_consumption='sensor.<your_device_energy>'",
|
|
1024
|
+
],
|
|
1025
|
+
context={"mode": "add_device"},
|
|
1026
|
+
)
|
|
1014
1027
|
|
|
1015
1028
|
target_key = "device_consumption_water" if water else "device_consumption"
|
|
1016
1029
|
|
|
@@ -1049,6 +1062,17 @@ class EnergyTools:
|
|
|
1049
1062
|
],
|
|
1050
1063
|
)
|
|
1051
1064
|
)
|
|
1065
|
+
# Empty/whitespace stat_consumption would search the prefs storage for
|
|
1066
|
+
# an empty match (always missing) and surface as a misleading
|
|
1067
|
+
# "Device with stat_consumption='' not found".
|
|
1068
|
+
validate_identifier_not_empty(
|
|
1069
|
+
stat_consumption,
|
|
1070
|
+
"stat_consumption",
|
|
1071
|
+
suggestions=[
|
|
1072
|
+
"Pass stat_consumption='sensor.<existing_device_energy>'",
|
|
1073
|
+
],
|
|
1074
|
+
context={"mode": "remove_device"},
|
|
1075
|
+
)
|
|
1052
1076
|
|
|
1053
1077
|
target_key = "device_consumption_water" if water else "device_consumption"
|
|
1054
1078
|
|