forked from homeassistant-ai/ha-mcp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtools_yaml_config.py
More file actions
282 lines (253 loc) · 11.4 KB
/
Copy pathtools_yaml_config.py
File metadata and controls
282 lines (253 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
"""
Managed YAML configuration editing tools for Home Assistant MCP Server.
Provides a structured, validated tool for editing YAML configuration files
(configuration.yaml and package files) for Home Assistant features that exist
only in YAML and have no REST/WebSocket API equivalent.
**Dependency:** Requires the ha_mcp_tools custom component to be installed.
The tools will gracefully fail with installation instructions if the component is not available.
Feature Flag: Set ENABLE_YAML_CONFIG_EDITING=true to enable.
"""
import logging
from typing import Annotated, Any
from fastmcp.exceptions import ToolError
from pydantic import Field
from ..config import get_global_settings
from ..errors import ErrorCode, create_error_response
from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
from .tools_filesystem import (
MCP_TOOLS_DOMAIN,
_assert_mcp_tools_available,
)
from .util_helpers import coerce_bool_param, unwrap_service_response
logger = logging.getLogger(__name__)
def register_yaml_config_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
"""Register YAML config editing tools with the MCP server.
Requires ENABLE_YAML_CONFIG_EDITING=true.
"""
settings = get_global_settings()
if not settings.enable_yaml_config_editing:
logger.debug(
"YAML config tools disabled (set ENABLE_YAML_CONFIG_EDITING=true to enable)"
)
return
logger.info("YAML config editing tools enabled")
@mcp.tool(
tags={"System"},
annotations={
"destructiveHint": True,
"idempotentHint": False,
"title": "Raw YAML Config Edit (Escape Hatch)",
},
)
@log_tool_usage
async def ha_config_set_yaml(
yaml_path: Annotated[
str,
Field(
description=(
"Top-level YAML key to modify. Only a narrow allowlist of "
"YAML-only integration keys is accepted (e.g., 'command_line', "
"'rest', 'shell_command', 'notify', 'utility_meter'). "
"STOP before using this for 'template' — the Template "
"config-flow helper (ha_set_config_entry_helper with "
"domain='template') supports state AND trigger-based template "
"sensors since HA 2024.x and is the correct path. Do NOT use "
"this tool for automations, scripts, scenes, or input_* "
"helpers — they have dedicated tools "
"(ha_config_set_automation, ha_config_set_script, "
"ha_config_set_scene, ha_config_set_helper)."
),
),
],
action: Annotated[
str,
Field(
description=(
"Action to perform: 'add' (insert/merge content under key), "
"'replace' (overwrite key with new content), or "
"'remove' (delete the key entirely)."
),
),
],
justification: Annotated[
str | None,
Field(
default=None,
description=(
"Required. Explain in one or two sentences why no dedicated "
"tool (ha_set_config_entry_helper for templates, "
"ha_config_set_automation, ha_config_set_script, "
"ha_config_set_scene, ha_config_set_helper) can accomplish "
"this task. Logged for auditing. If you cannot write a "
"concrete justification, you probably want a different tool."
),
),
] = None,
content: Annotated[
str | None,
Field(
default=None,
description=(
"YAML content for the value under yaml_path. Required for "
"'add' and 'replace' actions. Must be valid YAML."
),
),
] = None,
file: Annotated[
str,
Field(
default="configuration.yaml",
description=(
"Relative path to the YAML config file. Defaults to "
"'configuration.yaml'. Also supports 'packages/*.yaml'."
),
),
] = "configuration.yaml",
backup: Annotated[
bool | str,
Field(
default=True,
description=(
"Create a backup before editing. Defaults to True. "
"Backups are saved to www/yaml_backups/."
),
),
] = True,
) -> dict[str, Any]:
"""ESCAPE HATCH — raw YAML edit. Use only when NO dedicated tool fits.
This tool is the WRONG answer for almost everything. Before calling it,
confirm that NONE of these apply:
- Template sensors (state-based OR trigger-based) ->
ha_set_config_entry_helper with domain='template'. The Template
config-flow helper supports triggers, availability, attributes, device
class, unit of measurement, and state templates since HA 2024.x. It is
the correct path even for complex trigger-based sensors. Do NOT edit
the 'template:' YAML block for this.
- Automations -> ha_config_set_automation
- Scripts -> ha_config_set_script
- Scenes -> ha_config_set_scene
- Input helpers (input_boolean, input_number, input_text, input_select,
input_datetime, input_button, counter, timer, schedule) ->
ha_config_set_helper
- Groups, min/max, threshold, derivative, statistics, utility_meter
entities that can be created as helpers -> ha_config_set_helper
This tool is intended for YAML-only integrations that have no config-flow
or API equivalent: command_line sensors, REST sensors defined in
packages, shell_command entries, platform-based notify services, and
similar edge cases. If you are reaching for this to edit 'template:',
'automation:', 'script:', 'scene:', or 'input_*:', stop and use the
dedicated tool instead.
A ``justification`` is REQUIRED on every call and is logged for auditing.
The justification must explain why no dedicated tool fits. If you cannot
write a concrete justification, you are using the wrong tool.
Safeguards: file backup, YAML validation, top-level key allowlist,
path traversal blocking, post-edit config check.
Check 'post_action' in the response. Most keys require a full HA restart
('restart_required'). Only template, mqtt, and group support reload
('reload_available' with 'reload_service').
Preserves YAML comments on sibling keys, file-level comments, and Home
Assistant tags (!include, !secret, etc.). The 'replace' action
substitutes the subtree as-is, so comments from the old subtree do not
carry over.
"""
try:
# Validate action
valid_actions = ("add", "replace", "remove")
if action not in valid_actions:
raise_tool_error(
create_error_response(
ErrorCode.VALIDATION_INVALID_PARAMETER,
f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}",
suggestions=[
"Use action='add' to insert content under a key",
"Use action='replace' to overwrite a key's content",
"Use action='remove' to delete a key entirely",
],
)
)
# Require a non-empty justification. This is the primary friction
# that discourages reaching for this escape hatch — if the caller
# cannot articulate why no dedicated tool fits, they should use a
# dedicated tool instead. Mirrors the pattern from tools_code.py
# (ha_manage_custom_tool).
if not justification or not justification.strip():
raise_tool_error(
create_error_response(
ErrorCode.VALIDATION_INVALID_PARAMETER,
"justification is required for ha_config_set_yaml",
suggestions=[
"Explain why no dedicated tool can accomplish this "
"task (Template sensors -> "
"ha_set_config_entry_helper with "
"domain='template'; automations -> "
"ha_config_set_automation; scripts -> "
"ha_config_set_script; helpers -> "
"ha_config_set_helper)",
"If yaml_path is 'template', 'automation', "
"'script', 'scene', or 'input_*', use the "
"dedicated tool instead of this escape hatch",
],
)
)
# Validate content is provided for add/replace
if action in ("add", "replace") and not content:
raise_tool_error(
create_error_response(
ErrorCode.VALIDATION_INVALID_PARAMETER,
f"'content' is required for action '{action}'.",
suggestions=[
"Provide valid YAML content to insert or replace."
],
)
)
logger.info(
"ha_config_set_yaml invoked (yaml_path=%s, action=%s, file=%s) — justification: %s",
yaml_path,
action,
file,
justification[:200],
)
# Coerce boolean parameter
backup_bool = coerce_bool_param(backup, "backup", default=True)
# Check if custom component is available
await _assert_mcp_tools_available(client)
# Build service data
service_data: dict[str, Any] = {
"file": file,
"action": action,
"yaml_path": yaml_path,
"backup": backup_bool,
}
if content is not None:
service_data["content"] = content
# Call the custom component service
result = await client.call_service(
MCP_TOOLS_DOMAIN,
"edit_yaml_config",
service_data,
return_response=True,
)
if isinstance(result, dict):
result = unwrap_service_response(result)
if not result.get("success", True):
raise_tool_error(result)
return result
raise_tool_error(
create_error_response(
ErrorCode.SERVICE_CALL_FAILED,
"Unexpected response format from YAML config service",
context={"file": file},
)
)
except ToolError:
raise
except Exception as e:
exception_to_structured_error(
e,
context={
"tool": "ha_config_set_yaml",
"file": file,
"action": action,
"yaml_path": yaml_path,
},
)