ha-mcp-dev 7.2.0.dev352__tar.gz → 7.2.0.dev353__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.2.0.dev352/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev353}/PKG-INFO +1 -1
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/pyproject.toml +1 -8
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_config_automations.py +110 -106
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_filesystem.py +55 -33
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_hacs.py +142 -122
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_history.py +96 -67
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_resources.py +205 -184
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_traces.py +122 -96
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_updates.py +292 -285
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/README.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/setup.cfg +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/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.2.0.
|
|
7
|
+
version = "7.2.0.dev353"
|
|
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"
|
|
@@ -142,18 +142,11 @@ ignore = [
|
|
|
142
142
|
"src/ha_mcp/tools/registry.py" = ["C901"]
|
|
143
143
|
"src/ha_mcp/tools/smart_search.py" = ["C901"]
|
|
144
144
|
"src/ha_mcp/tools/tools_addons.py" = ["C901"]
|
|
145
|
-
"src/ha_mcp/tools/tools_config_automations.py" = ["C901"]
|
|
146
145
|
"src/ha_mcp/tools/tools_config_dashboards.py" = ["C901"]
|
|
147
146
|
"src/ha_mcp/tools/tools_config_helpers.py" = ["C901"]
|
|
148
147
|
"src/ha_mcp/tools/tools_entities.py" = ["C901"]
|
|
149
|
-
"src/ha_mcp/tools/tools_filesystem.py" = ["C901"]
|
|
150
|
-
"src/ha_mcp/tools/tools_hacs.py" = ["C901"]
|
|
151
|
-
"src/ha_mcp/tools/tools_history.py" = ["C901"]
|
|
152
148
|
"src/ha_mcp/tools/tools_registry.py" = ["C901"]
|
|
153
|
-
"src/ha_mcp/tools/tools_resources.py" = ["C901"]
|
|
154
149
|
"src/ha_mcp/tools/tools_search.py" = ["C901"]
|
|
155
|
-
"src/ha_mcp/tools/tools_traces.py" = ["C901"]
|
|
156
|
-
"src/ha_mcp/tools/tools_updates.py" = ["C901"]
|
|
157
150
|
"src/ha_mcp/tools/tools_utility.py" = ["C901"]
|
|
158
151
|
"src/ha_mcp/tools/util_helpers.py" = ["C901"]
|
|
159
152
|
|
{ha_mcp_dev-7.2.0.dev352 → ha_mcp_dev-7.2.0.dev353}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -9,6 +9,7 @@ import logging
|
|
|
9
9
|
from typing import Annotated, Any, cast
|
|
10
10
|
|
|
11
11
|
from fastmcp.exceptions import ToolError
|
|
12
|
+
from fastmcp.tools import tool
|
|
12
13
|
from pydantic import Field
|
|
13
14
|
|
|
14
15
|
from ..errors import (
|
|
@@ -22,7 +23,12 @@ from .best_practice_checker import (
|
|
|
22
23
|
from .best_practice_checker import (
|
|
23
24
|
get_skill_prefix as _get_skill_prefix,
|
|
24
25
|
)
|
|
25
|
-
from .helpers import
|
|
26
|
+
from .helpers import (
|
|
27
|
+
exception_to_structured_error,
|
|
28
|
+
log_tool_usage,
|
|
29
|
+
raise_tool_error,
|
|
30
|
+
register_tool_methods,
|
|
31
|
+
)
|
|
26
32
|
from .util_helpers import (
|
|
27
33
|
apply_entity_category,
|
|
28
34
|
coerce_bool_param,
|
|
@@ -48,7 +54,7 @@ def _normalize_automation_config(
|
|
|
48
54
|
and plural ('triggers', 'actions', 'conditions') field names in YAML,
|
|
49
55
|
but the API expects singular forms at the root level.
|
|
50
56
|
|
|
51
|
-
IMPORTANT: 'triggers'
|
|
57
|
+
IMPORTANT: 'triggers' -> 'trigger' and 'actions' -> 'action' normalization
|
|
52
58
|
is ONLY applied at the root level. Deeper in the tree these keys are either
|
|
53
59
|
invalid or semantically different, and normalizing them can produce keys
|
|
54
60
|
that Home Assistant rejects (e.g., 'action' inside a delay object).
|
|
@@ -67,8 +73,8 @@ def _normalize_automation_config(
|
|
|
67
73
|
in_choose_or_if: Whether we're inside a choose/if option that requires
|
|
68
74
|
'conditions' (plural) to remain unchanged
|
|
69
75
|
is_root: Whether this is the root-level automation config dict.
|
|
70
|
-
Only root level gets 'triggers'
|
|
71
|
-
'actions'
|
|
76
|
+
Only root level gets 'triggers'->'trigger' and
|
|
77
|
+
'actions'->'action' normalization.
|
|
72
78
|
|
|
73
79
|
Returns:
|
|
74
80
|
Normalized configuration with singular field names at root level,
|
|
@@ -100,14 +106,14 @@ def _normalize_automation_config(
|
|
|
100
106
|
# Build field mappings based on context
|
|
101
107
|
field_mappings: dict[str, str] = {}
|
|
102
108
|
|
|
103
|
-
# 'triggers'
|
|
109
|
+
# 'triggers' -> 'trigger' and 'actions' -> 'action' ONLY at root level.
|
|
104
110
|
# Deeper in the tree these keys are invalid and normalizing them produces
|
|
105
|
-
# keys HA rejects (e.g., 'action' inside a delay object
|
|
111
|
+
# keys HA rejects (e.g., 'action' inside a delay object -- see issue #498).
|
|
106
112
|
if is_root:
|
|
107
113
|
field_mappings["triggers"] = "trigger"
|
|
108
114
|
field_mappings["actions"] = "action"
|
|
109
115
|
|
|
110
|
-
# 'sequences'
|
|
116
|
+
# 'sequences' -> 'sequence' is safe at any level (only meaningful in choose options)
|
|
111
117
|
field_mappings["sequences"] = "sequence"
|
|
112
118
|
|
|
113
119
|
# Only add 'conditions' mapping if NOT inside a choose/if option
|
|
@@ -180,34 +186,13 @@ def _normalize_config_for_roundtrip(config: dict[str, Any]) -> dict[str, Any]:
|
|
|
180
186
|
return cast(dict[str, Any], normalized)
|
|
181
187
|
|
|
182
188
|
|
|
183
|
-
|
|
184
|
-
"""
|
|
185
|
-
Strip empty trigger/action/condition arrays from automation config.
|
|
186
|
-
|
|
187
|
-
Blueprint-based automations should not have trigger/action/condition fields
|
|
188
|
-
since these come from the blueprint itself. If empty arrays are present,
|
|
189
|
-
they override the blueprint's configuration and break the automation.
|
|
190
|
-
|
|
191
|
-
Args:
|
|
192
|
-
config: Automation configuration dict
|
|
193
|
-
|
|
194
|
-
Returns:
|
|
195
|
-
Configuration with empty trigger/action/condition arrays removed
|
|
196
|
-
"""
|
|
197
|
-
cleaned = config.copy()
|
|
198
|
-
|
|
199
|
-
# Remove empty arrays for blueprint automations
|
|
200
|
-
for field in ["trigger", "action", "condition"]:
|
|
201
|
-
if field in cleaned and cleaned[field] == []:
|
|
202
|
-
del cleaned[field]
|
|
203
|
-
|
|
204
|
-
return cleaned
|
|
205
|
-
|
|
189
|
+
class AutomationConfigTools:
|
|
190
|
+
"""Configuration management tools for Home Assistant automations."""
|
|
206
191
|
|
|
207
|
-
def
|
|
208
|
-
|
|
192
|
+
def __init__(self, client: Any) -> None:
|
|
193
|
+
self._client = client
|
|
209
194
|
|
|
210
|
-
async def _resolve_automation_entity_id(identifier: str) -> str | None:
|
|
195
|
+
async def _resolve_automation_entity_id(self, identifier: str) -> str | None:
|
|
211
196
|
"""Resolve an automation identifier to its entity_id.
|
|
212
197
|
|
|
213
198
|
If identifier is already an entity_id (starts with "automation."),
|
|
@@ -217,7 +202,7 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
217
202
|
if identifier.startswith("automation."):
|
|
218
203
|
return identifier
|
|
219
204
|
try:
|
|
220
|
-
states = await
|
|
205
|
+
states = await self._client.get_states()
|
|
221
206
|
for state in states:
|
|
222
207
|
if (
|
|
223
208
|
state.get("entity_id", "").startswith("automation.")
|
|
@@ -228,16 +213,18 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
228
213
|
logger.debug(f"Failed to resolve entity_id for automation {identifier}: {e}")
|
|
229
214
|
return None
|
|
230
215
|
|
|
231
|
-
@
|
|
216
|
+
@tool(
|
|
217
|
+
name="ha_config_get_automation",
|
|
232
218
|
tags={"Automations"},
|
|
233
219
|
annotations={
|
|
234
220
|
"idempotentHint": True,
|
|
235
221
|
"readOnlyHint": True,
|
|
236
|
-
"title": "Get Automation Config"
|
|
237
|
-
}
|
|
222
|
+
"title": "Get Automation Config",
|
|
223
|
+
},
|
|
238
224
|
)
|
|
239
225
|
@log_tool_usage
|
|
240
226
|
async def ha_config_get_automation(
|
|
227
|
+
self,
|
|
241
228
|
identifier: Annotated[
|
|
242
229
|
str,
|
|
243
230
|
Field(
|
|
@@ -257,14 +244,14 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
257
244
|
For comprehensive automation documentation, use ha_get_skill_home_assistant_best_practices.
|
|
258
245
|
"""
|
|
259
246
|
try:
|
|
260
|
-
config_result = await
|
|
261
|
-
# Normalize config for round-trip compatibility (GET
|
|
247
|
+
config_result = await self._client.get_automation_config(identifier)
|
|
248
|
+
# Normalize config for round-trip compatibility (GET -> SET)
|
|
262
249
|
normalized_config = _normalize_config_for_roundtrip(config_result)
|
|
263
250
|
|
|
264
251
|
# Resolve entity_id and fetch category from entity registry
|
|
265
|
-
entity_id = await _resolve_automation_entity_id(identifier)
|
|
252
|
+
entity_id = await self._resolve_automation_entity_id(identifier)
|
|
266
253
|
if entity_id:
|
|
267
|
-
cat_id = await fetch_entity_category(
|
|
254
|
+
cat_id = await fetch_entity_category(self._client, entity_id, "automation")
|
|
268
255
|
if cat_id:
|
|
269
256
|
normalized_config["category"] = cat_id
|
|
270
257
|
|
|
@@ -305,15 +292,17 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
305
292
|
],
|
|
306
293
|
)
|
|
307
294
|
|
|
308
|
-
@
|
|
295
|
+
@tool(
|
|
296
|
+
name="ha_config_set_automation",
|
|
309
297
|
tags={"Automations"},
|
|
310
298
|
annotations={
|
|
311
299
|
"destructiveHint": True,
|
|
312
|
-
"title": "Create or Update Automation"
|
|
313
|
-
}
|
|
300
|
+
"title": "Create or Update Automation",
|
|
301
|
+
},
|
|
314
302
|
)
|
|
315
303
|
@log_tool_usage
|
|
316
304
|
async def ha_config_set_automation(
|
|
305
|
+
self,
|
|
317
306
|
config: Annotated[
|
|
318
307
|
str | dict[str, Any],
|
|
319
308
|
Field(
|
|
@@ -437,7 +426,7 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
437
426
|
}
|
|
438
427
|
}
|
|
439
428
|
}
|
|
440
|
-
|
|
429
|
+
)
|
|
441
430
|
|
|
442
431
|
PREFER NATIVE SOLUTIONS OVER TEMPLATES:
|
|
443
432
|
Before using template triggers/conditions/actions, check if a native option exists:
|
|
@@ -463,25 +452,7 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
463
452
|
"""
|
|
464
453
|
bp_warnings: list[str] = []
|
|
465
454
|
try:
|
|
466
|
-
|
|
467
|
-
try:
|
|
468
|
-
parsed_config = parse_json_param(config, "config")
|
|
469
|
-
except ValueError as e:
|
|
470
|
-
raise_tool_error(create_validation_error(
|
|
471
|
-
f"Invalid config parameter: {e}",
|
|
472
|
-
parameter="config",
|
|
473
|
-
invalid_json=True,
|
|
474
|
-
))
|
|
475
|
-
|
|
476
|
-
# Ensure config is a dict
|
|
477
|
-
if parsed_config is None or not isinstance(parsed_config, dict):
|
|
478
|
-
raise_tool_error(create_validation_error(
|
|
479
|
-
"Config parameter must be a JSON object",
|
|
480
|
-
parameter="config",
|
|
481
|
-
details=f"Received type: {type(parsed_config).__name__}",
|
|
482
|
-
))
|
|
483
|
-
|
|
484
|
-
config_dict = cast(dict[str, Any], parsed_config)
|
|
455
|
+
config_dict = self._parse_and_validate_config(config)
|
|
485
456
|
|
|
486
457
|
# Extract category before sending to HA REST API (which rejects unknown keys).
|
|
487
458
|
# Parameter takes precedence over config dict value.
|
|
@@ -492,48 +463,20 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
492
463
|
config_dict = _normalize_automation_config(config_dict)
|
|
493
464
|
|
|
494
465
|
# Validate required fields based on automation type
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
required_fields = ["alias"]
|
|
498
|
-
# Strip empty trigger/action/condition arrays that would override blueprint
|
|
499
|
-
config_dict = _strip_empty_automation_fields(config_dict)
|
|
500
|
-
else:
|
|
501
|
-
required_fields = ["alias", "trigger", "action"]
|
|
502
|
-
|
|
503
|
-
missing_fields = [f for f in required_fields if f not in config_dict]
|
|
504
|
-
if missing_fields:
|
|
505
|
-
raise_tool_error(create_config_error(
|
|
506
|
-
f"Missing required fields: {', '.join(missing_fields)}",
|
|
507
|
-
identifier=identifier,
|
|
508
|
-
missing_fields=missing_fields,
|
|
509
|
-
))
|
|
510
|
-
|
|
511
|
-
# Prevent duplicate creation when config contains an existing automation id
|
|
512
|
-
if identifier is None and "id" in config_dict:
|
|
513
|
-
existing_id = config_dict["id"]
|
|
514
|
-
raise_tool_error(create_validation_error(
|
|
515
|
-
f"Config contains 'id' field ('{existing_id}') but no identifier was provided. "
|
|
516
|
-
"This would create a duplicate automation instead of updating the existing one.",
|
|
517
|
-
parameter="identifier",
|
|
518
|
-
details=f"To update, pass identifier='{existing_id}' (or the automation's entity_id). "
|
|
519
|
-
"To create a genuinely new automation, remove the 'id' field from the config.",
|
|
520
|
-
))
|
|
521
|
-
|
|
522
|
-
# Pre-check for best-practice issues (used for both success
|
|
523
|
-
# warnings and error enrichment if the API call fails).
|
|
466
|
+
self._validate_required_fields(config_dict, identifier)
|
|
467
|
+
|
|
524
468
|
# Pre-check for best-practice issues.
|
|
525
469
|
bp_warnings = _check_best_practices(
|
|
526
470
|
config_dict, skill_prefix=_get_skill_prefix()
|
|
527
471
|
)
|
|
528
472
|
|
|
529
|
-
result = await
|
|
473
|
+
result = await self._client.upsert_automation_config(config_dict, identifier)
|
|
530
474
|
|
|
531
475
|
# If the client could not verify the entity was registered, warn but don't hard-fail.
|
|
532
|
-
# The automation may have been created but not yet visible (slow hardware, reload needed).
|
|
533
476
|
if result.get("entity_not_verified"):
|
|
534
477
|
result["warning"] = (
|
|
535
478
|
"Automation was submitted to Home Assistant but the entity was not found "
|
|
536
|
-
"after polling. The automation may still have been created
|
|
479
|
+
"after polling. The automation may still have been created -- check Home "
|
|
537
480
|
"Assistant logs and try reloading automations. Common causes: "
|
|
538
481
|
"automations.yaml vs automation.yaml filename mismatch, invalid config "
|
|
539
482
|
"that HA accepted but failed to load, or slow hardware."
|
|
@@ -543,12 +486,12 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
543
486
|
# Wait for automation to be queryable
|
|
544
487
|
wait_bool = coerce_bool_param(wait, "wait", default=True)
|
|
545
488
|
entity_id = result.get("entity_id")
|
|
546
|
-
# On updates, entity_id may not be in the result
|
|
489
|
+
# On updates, entity_id may not be in the result -- derive from identifier
|
|
547
490
|
if not entity_id and identifier and identifier.startswith("automation."):
|
|
548
491
|
entity_id = identifier
|
|
549
492
|
if wait_bool and entity_id:
|
|
550
493
|
try:
|
|
551
|
-
registered = await wait_for_entity_registered(
|
|
494
|
+
registered = await wait_for_entity_registered(self._client, entity_id)
|
|
552
495
|
if not registered:
|
|
553
496
|
result["warning"] = f"Automation created but {entity_id} not yet queryable. It may take a moment to become available."
|
|
554
497
|
except Exception as e:
|
|
@@ -557,7 +500,7 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
557
500
|
# Apply category to entity registry if provided
|
|
558
501
|
if effective_category and entity_id:
|
|
559
502
|
await apply_entity_category(
|
|
560
|
-
|
|
503
|
+
self._client, entity_id, effective_category, "automation", result, "automation"
|
|
561
504
|
)
|
|
562
505
|
|
|
563
506
|
if bp_warnings:
|
|
@@ -590,16 +533,72 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
590
533
|
suggestions=suggestions,
|
|
591
534
|
)
|
|
592
535
|
|
|
593
|
-
@
|
|
536
|
+
@staticmethod
|
|
537
|
+
def _parse_and_validate_config(config: str | dict[str, Any]) -> dict[str, Any]:
|
|
538
|
+
"""Parse JSON config and validate it is a dict."""
|
|
539
|
+
try:
|
|
540
|
+
parsed_config = parse_json_param(config, "config")
|
|
541
|
+
except ValueError as e:
|
|
542
|
+
raise_tool_error(create_validation_error(
|
|
543
|
+
f"Invalid config parameter: {e}",
|
|
544
|
+
parameter="config",
|
|
545
|
+
invalid_json=True,
|
|
546
|
+
))
|
|
547
|
+
|
|
548
|
+
if parsed_config is None or not isinstance(parsed_config, dict):
|
|
549
|
+
raise_tool_error(create_validation_error(
|
|
550
|
+
"Config parameter must be a JSON object",
|
|
551
|
+
parameter="config",
|
|
552
|
+
details=f"Received type: {type(parsed_config).__name__}",
|
|
553
|
+
))
|
|
554
|
+
|
|
555
|
+
return cast(dict[str, Any], parsed_config)
|
|
556
|
+
|
|
557
|
+
@staticmethod
|
|
558
|
+
def _validate_required_fields(
|
|
559
|
+
config_dict: dict[str, Any], identifier: str | None
|
|
560
|
+
) -> None:
|
|
561
|
+
"""Validate required fields and prevent duplicate creation."""
|
|
562
|
+
if "use_blueprint" in config_dict:
|
|
563
|
+
required_fields = ["alias"]
|
|
564
|
+
# Strip empty trigger/action/condition arrays that would override blueprint
|
|
565
|
+
for field in ["trigger", "action", "condition"]:
|
|
566
|
+
if field in config_dict and config_dict[field] == []:
|
|
567
|
+
del config_dict[field]
|
|
568
|
+
else:
|
|
569
|
+
required_fields = ["alias", "trigger", "action"]
|
|
570
|
+
|
|
571
|
+
missing_fields = [f for f in required_fields if f not in config_dict]
|
|
572
|
+
if missing_fields:
|
|
573
|
+
raise_tool_error(create_config_error(
|
|
574
|
+
f"Missing required fields: {', '.join(missing_fields)}",
|
|
575
|
+
identifier=identifier,
|
|
576
|
+
missing_fields=missing_fields,
|
|
577
|
+
))
|
|
578
|
+
|
|
579
|
+
# Prevent duplicate creation when config contains an existing automation id
|
|
580
|
+
if identifier is None and "id" in config_dict:
|
|
581
|
+
existing_id = config_dict["id"]
|
|
582
|
+
raise_tool_error(create_validation_error(
|
|
583
|
+
f"Config contains 'id' field ('{existing_id}') but no identifier was provided. "
|
|
584
|
+
"This would create a duplicate automation instead of updating the existing one.",
|
|
585
|
+
parameter="identifier",
|
|
586
|
+
details=f"To update, pass identifier='{existing_id}' (or the automation's entity_id). "
|
|
587
|
+
"To create a genuinely new automation, remove the 'id' field from the config.",
|
|
588
|
+
))
|
|
589
|
+
|
|
590
|
+
@tool(
|
|
591
|
+
name="ha_config_remove_automation",
|
|
594
592
|
tags={"Automations"},
|
|
595
593
|
annotations={
|
|
596
594
|
"destructiveHint": True,
|
|
597
595
|
"idempotentHint": True,
|
|
598
|
-
"title": "Remove Automation"
|
|
599
|
-
}
|
|
596
|
+
"title": "Remove Automation",
|
|
597
|
+
},
|
|
600
598
|
)
|
|
601
599
|
@log_tool_usage
|
|
602
600
|
async def ha_config_remove_automation(
|
|
601
|
+
self,
|
|
603
602
|
identifier: Annotated[
|
|
604
603
|
str,
|
|
605
604
|
Field(
|
|
@@ -625,19 +624,19 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
625
624
|
"""
|
|
626
625
|
try:
|
|
627
626
|
# Resolve entity_id for wait verification (identifier may be a unique_id)
|
|
628
|
-
entity_id_for_wait = await _resolve_automation_entity_id(identifier)
|
|
627
|
+
entity_id_for_wait = await self._resolve_automation_entity_id(identifier)
|
|
629
628
|
if not entity_id_for_wait:
|
|
630
629
|
logger.warning(
|
|
631
|
-
f"Could not resolve unique_id '{identifier}' to entity_id
|
|
630
|
+
f"Could not resolve unique_id '{identifier}' to entity_id -- wait verification will be skipped"
|
|
632
631
|
)
|
|
633
632
|
|
|
634
|
-
result = await
|
|
633
|
+
result = await self._client.delete_automation_config(identifier)
|
|
635
634
|
|
|
636
635
|
# Wait for entity to be removed
|
|
637
636
|
wait_bool = coerce_bool_param(wait, "wait", default=True)
|
|
638
637
|
if wait_bool and entity_id_for_wait:
|
|
639
638
|
try:
|
|
640
|
-
removed = await wait_for_entity_removed(
|
|
639
|
+
removed = await wait_for_entity_removed(self._client, entity_id_for_wait)
|
|
641
640
|
if not removed:
|
|
642
641
|
result["warning"] = f"Deletion confirmed by API but {entity_id_for_wait} may still appear briefly."
|
|
643
642
|
except Exception as e:
|
|
@@ -668,3 +667,8 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
668
667
|
"Check Home Assistant connection",
|
|
669
668
|
]
|
|
670
669
|
raise_tool_error(error_response)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
673
|
+
"""Register Home Assistant automation configuration tools."""
|
|
674
|
+
register_tool_methods(mcp, AutomationConfigTools(client))
|
|
@@ -19,10 +19,16 @@ import os
|
|
|
19
19
|
from typing import Annotated, Any
|
|
20
20
|
|
|
21
21
|
from fastmcp.exceptions import ToolError
|
|
22
|
+
from fastmcp.tools import tool
|
|
22
23
|
from pydantic import Field
|
|
23
24
|
|
|
24
25
|
from ..errors import ErrorCode, create_error_response
|
|
25
|
-
from .helpers import
|
|
26
|
+
from .helpers import (
|
|
27
|
+
exception_to_structured_error,
|
|
28
|
+
log_tool_usage,
|
|
29
|
+
raise_tool_error,
|
|
30
|
+
register_tool_methods,
|
|
31
|
+
)
|
|
26
32
|
from .util_helpers import coerce_bool_param, coerce_int_param, unwrap_service_response
|
|
27
33
|
|
|
28
34
|
logger = logging.getLogger(__name__)
|
|
@@ -87,29 +93,23 @@ async def _assert_mcp_tools_available(client: Any) -> None:
|
|
|
87
93
|
))
|
|
88
94
|
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
"""
|
|
92
|
-
|
|
93
|
-
This function only registers tools if the feature flag is enabled.
|
|
94
|
-
Set HAMCP_ENABLE_FILESYSTEM_TOOLS=true to enable.
|
|
95
|
-
"""
|
|
96
|
-
if not is_filesystem_tools_enabled():
|
|
97
|
-
logger.debug(
|
|
98
|
-
f"Filesystem tools disabled (set {FEATURE_FLAG}=true to enable)"
|
|
99
|
-
)
|
|
100
|
-
return
|
|
96
|
+
class FilesystemTools:
|
|
97
|
+
"""Filesystem access tools for Home Assistant."""
|
|
101
98
|
|
|
102
|
-
|
|
99
|
+
def __init__(self, client: Any) -> None:
|
|
100
|
+
self._client = client
|
|
103
101
|
|
|
104
|
-
@
|
|
102
|
+
@tool(
|
|
103
|
+
name="ha_list_files",
|
|
105
104
|
tags={"Files"},
|
|
106
105
|
annotations={
|
|
107
106
|
"readOnlyHint": True,
|
|
108
|
-
"title": "List Files"
|
|
109
|
-
}
|
|
107
|
+
"title": "List Files",
|
|
108
|
+
},
|
|
110
109
|
)
|
|
111
110
|
@log_tool_usage
|
|
112
111
|
async def ha_list_files(
|
|
112
|
+
self,
|
|
113
113
|
path: Annotated[
|
|
114
114
|
str,
|
|
115
115
|
Field(
|
|
@@ -158,7 +158,7 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
158
158
|
"""
|
|
159
159
|
try:
|
|
160
160
|
# Check if custom component is available
|
|
161
|
-
await _assert_mcp_tools_available(
|
|
161
|
+
await _assert_mcp_tools_available(self._client)
|
|
162
162
|
|
|
163
163
|
# Build service data
|
|
164
164
|
service_data: dict[str, Any] = {"path": path}
|
|
@@ -166,7 +166,7 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
166
166
|
service_data["pattern"] = pattern
|
|
167
167
|
|
|
168
168
|
# Call the custom component service
|
|
169
|
-
result = await
|
|
169
|
+
result = await self._client.call_service(
|
|
170
170
|
MCP_TOOLS_DOMAIN,
|
|
171
171
|
"list_files",
|
|
172
172
|
service_data,
|
|
@@ -193,15 +193,17 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
193
193
|
context={"tool": "ha_list_files", "path": path, "pattern": pattern},
|
|
194
194
|
)
|
|
195
195
|
|
|
196
|
-
@
|
|
196
|
+
@tool(
|
|
197
|
+
name="ha_read_file",
|
|
197
198
|
tags={"Files"},
|
|
198
199
|
annotations={
|
|
199
200
|
"readOnlyHint": True,
|
|
200
|
-
"title": "Read File"
|
|
201
|
-
}
|
|
201
|
+
"title": "Read File",
|
|
202
|
+
},
|
|
202
203
|
)
|
|
203
204
|
@log_tool_usage
|
|
204
205
|
async def ha_read_file(
|
|
206
|
+
self,
|
|
205
207
|
path: Annotated[
|
|
206
208
|
str,
|
|
207
209
|
Field(
|
|
@@ -270,7 +272,7 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
270
272
|
)
|
|
271
273
|
|
|
272
274
|
# Check if custom component is available
|
|
273
|
-
await _assert_mcp_tools_available(
|
|
275
|
+
await _assert_mcp_tools_available(self._client)
|
|
274
276
|
|
|
275
277
|
# Build service data
|
|
276
278
|
service_data: dict[str, Any] = {"path": path}
|
|
@@ -278,7 +280,7 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
278
280
|
service_data["tail_lines"] = tail_lines_int
|
|
279
281
|
|
|
280
282
|
# Call the custom component service
|
|
281
|
-
result = await
|
|
283
|
+
result = await self._client.call_service(
|
|
282
284
|
MCP_TOOLS_DOMAIN,
|
|
283
285
|
"read_file",
|
|
284
286
|
service_data,
|
|
@@ -304,15 +306,17 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
304
306
|
context={"tool": "ha_read_file", "path": path},
|
|
305
307
|
)
|
|
306
308
|
|
|
307
|
-
@
|
|
309
|
+
@tool(
|
|
310
|
+
name="ha_write_file",
|
|
308
311
|
tags={"Files"},
|
|
309
312
|
annotations={
|
|
310
313
|
"destructiveHint": True,
|
|
311
|
-
"title": "Write File"
|
|
312
|
-
}
|
|
314
|
+
"title": "Write File",
|
|
315
|
+
},
|
|
313
316
|
)
|
|
314
317
|
@log_tool_usage
|
|
315
318
|
async def ha_write_file(
|
|
319
|
+
self,
|
|
316
320
|
path: Annotated[
|
|
317
321
|
str,
|
|
318
322
|
Field(
|
|
@@ -396,7 +400,7 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
396
400
|
create_dirs_bool = coerce_bool_param(create_dirs, "create_dirs", default=True)
|
|
397
401
|
|
|
398
402
|
# Check if custom component is available
|
|
399
|
-
await _assert_mcp_tools_available(
|
|
403
|
+
await _assert_mcp_tools_available(self._client)
|
|
400
404
|
|
|
401
405
|
# Build service data
|
|
402
406
|
service_data: dict[str, Any] = {
|
|
@@ -407,7 +411,7 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
407
411
|
}
|
|
408
412
|
|
|
409
413
|
# Call the custom component service
|
|
410
|
-
result = await
|
|
414
|
+
result = await self._client.call_service(
|
|
411
415
|
MCP_TOOLS_DOMAIN,
|
|
412
416
|
"write_file",
|
|
413
417
|
service_data,
|
|
@@ -433,15 +437,17 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
433
437
|
context={"tool": "ha_write_file", "path": path},
|
|
434
438
|
)
|
|
435
439
|
|
|
436
|
-
@
|
|
440
|
+
@tool(
|
|
441
|
+
name="ha_delete_file",
|
|
437
442
|
tags={"Files"},
|
|
438
443
|
annotations={
|
|
439
444
|
"destructiveHint": True,
|
|
440
|
-
"title": "Delete File"
|
|
441
|
-
}
|
|
445
|
+
"title": "Delete File",
|
|
446
|
+
},
|
|
442
447
|
)
|
|
443
448
|
@log_tool_usage
|
|
444
449
|
async def ha_delete_file(
|
|
450
|
+
self,
|
|
445
451
|
path: Annotated[
|
|
446
452
|
str,
|
|
447
453
|
Field(
|
|
@@ -510,13 +516,13 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
510
516
|
)
|
|
511
517
|
|
|
512
518
|
# Check if custom component is available
|
|
513
|
-
await _assert_mcp_tools_available(
|
|
519
|
+
await _assert_mcp_tools_available(self._client)
|
|
514
520
|
|
|
515
521
|
# Build service data
|
|
516
522
|
service_data: dict[str, Any] = {"path": path}
|
|
517
523
|
|
|
518
524
|
# Call the custom component service
|
|
519
|
-
result = await
|
|
525
|
+
result = await self._client.call_service(
|
|
520
526
|
MCP_TOOLS_DOMAIN,
|
|
521
527
|
"delete_file",
|
|
522
528
|
service_data,
|
|
@@ -541,3 +547,19 @@ def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
541
547
|
e,
|
|
542
548
|
context={"tool": "ha_delete_file", "path": path},
|
|
543
549
|
)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def register_filesystem_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
553
|
+
"""Register filesystem access tools with the MCP server.
|
|
554
|
+
|
|
555
|
+
This function only registers tools if the feature flag is enabled.
|
|
556
|
+
Set HAMCP_ENABLE_FILESYSTEM_TOOLS=true to enable.
|
|
557
|
+
"""
|
|
558
|
+
if not is_filesystem_tools_enabled():
|
|
559
|
+
logger.debug(
|
|
560
|
+
f"Filesystem tools disabled (set {FEATURE_FLAG}=true to enable)"
|
|
561
|
+
)
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
logger.info("Filesystem tools enabled via feature flag")
|
|
565
|
+
register_tool_methods(mcp, FilesystemTools(client))
|