ha-mcp-dev 6.3.1.dev137__py3-none-any.whl
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/__init__.py +51 -0
- ha_mcp/__main__.py +753 -0
- ha_mcp/_pypi_marker +0 -0
- ha_mcp/auth/__init__.py +10 -0
- ha_mcp/auth/consent_form.py +501 -0
- ha_mcp/auth/provider.py +786 -0
- ha_mcp/client/__init__.py +10 -0
- ha_mcp/client/rest_client.py +924 -0
- ha_mcp/client/websocket_client.py +656 -0
- ha_mcp/client/websocket_listener.py +340 -0
- ha_mcp/config.py +175 -0
- ha_mcp/errors.py +399 -0
- ha_mcp/py.typed +0 -0
- ha_mcp/resources/card_types.json +48 -0
- ha_mcp/resources/dashboard_guide.md +573 -0
- ha_mcp/server.py +189 -0
- ha_mcp/smoke_test.py +108 -0
- ha_mcp/tools/__init__.py +11 -0
- ha_mcp/tools/backup.py +457 -0
- ha_mcp/tools/device_control.py +687 -0
- ha_mcp/tools/enhanced.py +191 -0
- ha_mcp/tools/helpers.py +184 -0
- ha_mcp/tools/registry.py +199 -0
- ha_mcp/tools/smart_search.py +797 -0
- ha_mcp/tools/tools_addons.py +343 -0
- ha_mcp/tools/tools_areas.py +478 -0
- ha_mcp/tools/tools_blueprints.py +279 -0
- ha_mcp/tools/tools_bug_report.py +570 -0
- ha_mcp/tools/tools_calendar.py +391 -0
- ha_mcp/tools/tools_camera.py +149 -0
- ha_mcp/tools/tools_config_automations.py +508 -0
- ha_mcp/tools/tools_config_dashboards.py +1502 -0
- ha_mcp/tools/tools_config_entry_flow.py +223 -0
- ha_mcp/tools/tools_config_helpers.py +925 -0
- ha_mcp/tools/tools_config_info.py +258 -0
- ha_mcp/tools/tools_config_scripts.py +267 -0
- ha_mcp/tools/tools_entities.py +76 -0
- ha_mcp/tools/tools_filesystem.py +589 -0
- ha_mcp/tools/tools_groups.py +306 -0
- ha_mcp/tools/tools_hacs.py +787 -0
- ha_mcp/tools/tools_history.py +714 -0
- ha_mcp/tools/tools_integrations.py +297 -0
- ha_mcp/tools/tools_labels.py +713 -0
- ha_mcp/tools/tools_mcp_component.py +305 -0
- ha_mcp/tools/tools_registry.py +1115 -0
- ha_mcp/tools/tools_resources.py +622 -0
- ha_mcp/tools/tools_search.py +573 -0
- ha_mcp/tools/tools_service.py +211 -0
- ha_mcp/tools/tools_services.py +352 -0
- ha_mcp/tools/tools_system.py +363 -0
- ha_mcp/tools/tools_todo.py +530 -0
- ha_mcp/tools/tools_traces.py +496 -0
- ha_mcp/tools/tools_updates.py +476 -0
- ha_mcp/tools/tools_utility.py +519 -0
- ha_mcp/tools/tools_voice_assistant.py +345 -0
- ha_mcp/tools/tools_zones.py +387 -0
- ha_mcp/tools/util_helpers.py +199 -0
- ha_mcp/utils/__init__.py +30 -0
- ha_mcp/utils/domain_handlers.py +380 -0
- ha_mcp/utils/fuzzy_search.py +339 -0
- ha_mcp/utils/operation_manager.py +442 -0
- ha_mcp/utils/usage_logger.py +290 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/METADATA +229 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/RECORD +71 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/WHEEL +5 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/entry_points.txt +7 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/licenses/LICENSE +21 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_constants.py +15 -0
- tests/test_env_manager.py +336 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Tools Component installer for Home Assistant.
|
|
3
|
+
|
|
4
|
+
This module provides the ha_install_mcp_tools tool which installs the
|
|
5
|
+
ha_mcp_tools custom component via HACS. This enables additional services
|
|
6
|
+
that are not available through standard Home Assistant APIs.
|
|
7
|
+
|
|
8
|
+
Feature Flag: Set HAMCP_ENABLE_MCP_TOOLS_INSTALLER=true to enable this tool.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from typing import Annotated, Any
|
|
14
|
+
|
|
15
|
+
from pydantic import Field
|
|
16
|
+
|
|
17
|
+
from .helpers import exception_to_structured_error, log_tool_usage
|
|
18
|
+
from .util_helpers import add_timezone_metadata
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Feature flag - disabled by default for silent launch
|
|
23
|
+
FEATURE_FLAG = "HAMCP_ENABLE_CUSTOM_COMPONENT_INTEGRATION"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_custom_component_integration_enabled() -> bool:
|
|
27
|
+
"""Check if the custom component integration feature is enabled."""
|
|
28
|
+
value = os.getenv(FEATURE_FLAG, "").lower()
|
|
29
|
+
return value in ("true", "1", "yes", "on")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Constants for ha_mcp_tools custom component
|
|
33
|
+
# TODO: Switch to "homeassistant-ai/ha-mcp" after hacs.json is on default branch
|
|
34
|
+
MCP_TOOLS_REPO = "julienld/ha-mcp-test-custom-component"
|
|
35
|
+
MCP_TOOLS_DOMAIN = "ha_mcp_tools"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def register_mcp_component_tools(mcp, client, **kwargs):
|
|
39
|
+
"""Register MCP component installation tools.
|
|
40
|
+
|
|
41
|
+
This function only registers tools if the feature flag is enabled.
|
|
42
|
+
Set HAMCP_ENABLE_CUSTOM_COMPONENT_INTEGRATION=true to enable.
|
|
43
|
+
"""
|
|
44
|
+
if not is_custom_component_integration_enabled():
|
|
45
|
+
logger.debug(
|
|
46
|
+
f"MCP tools installer disabled (set {FEATURE_FLAG}=true to enable)"
|
|
47
|
+
)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
logger.info("MCP tools installer enabled via feature flag")
|
|
51
|
+
|
|
52
|
+
# Import HACS helpers - we depend on HACS functionality
|
|
53
|
+
from .tools_hacs import _check_hacs_available, CATEGORY_MAP
|
|
54
|
+
|
|
55
|
+
@mcp.tool(
|
|
56
|
+
annotations={
|
|
57
|
+
"destructiveHint": True,
|
|
58
|
+
"tags": ["mcp", "management", "installation"],
|
|
59
|
+
"title": "Install MCP Tools Component",
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
@log_tool_usage
|
|
63
|
+
async def ha_install_mcp_tools(
|
|
64
|
+
restart: Annotated[
|
|
65
|
+
bool,
|
|
66
|
+
Field(
|
|
67
|
+
default=False,
|
|
68
|
+
description="Whether to restart Home Assistant after installation (required for integration to load)",
|
|
69
|
+
),
|
|
70
|
+
] = False,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
"""Install the ha_mcp_tools custom component via HACS.
|
|
73
|
+
|
|
74
|
+
This tool installs the ha_mcp_tools custom component which provides
|
|
75
|
+
advanced services not available through standard Home Assistant APIs:
|
|
76
|
+
|
|
77
|
+
**Available Services (after installation):**
|
|
78
|
+
- `ha_mcp_tools.list_files`: List files in allowed directories (www/, themes/)
|
|
79
|
+
- More services coming soon: file write, backup cleanup, event buffer, etc.
|
|
80
|
+
|
|
81
|
+
**Installation Process:**
|
|
82
|
+
1. Checks if HACS is available
|
|
83
|
+
2. Checks if ha_mcp_tools is already installed
|
|
84
|
+
3. Adds the repository to HACS if not present
|
|
85
|
+
4. Downloads and installs the component
|
|
86
|
+
5. Optionally restarts Home Assistant
|
|
87
|
+
|
|
88
|
+
**Note:** A restart is required for the integration to load and become available.
|
|
89
|
+
Set `restart=True` to automatically restart, or manually restart later.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
restart: Whether to restart Home Assistant after installation (default: False)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Installation status and next steps.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
# Check if HACS is available
|
|
99
|
+
is_available, error_msg = await _check_hacs_available(client)
|
|
100
|
+
if not is_available:
|
|
101
|
+
return await add_timezone_metadata(
|
|
102
|
+
client,
|
|
103
|
+
{
|
|
104
|
+
"success": False,
|
|
105
|
+
"error": error_msg,
|
|
106
|
+
"error_code": "HACS_NOT_AVAILABLE",
|
|
107
|
+
"suggestions": [
|
|
108
|
+
"Install HACS from https://hacs.xyz/",
|
|
109
|
+
"Ensure Home Assistant has been restarted after HACS installation",
|
|
110
|
+
"Check Home Assistant logs for HACS errors",
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
from ..client.websocket_client import get_websocket_client
|
|
116
|
+
|
|
117
|
+
ws_client = await get_websocket_client()
|
|
118
|
+
|
|
119
|
+
# Check if HACS is fully functional (not disabled)
|
|
120
|
+
info_response = await ws_client.send_command("hacs/info")
|
|
121
|
+
if info_response.get("success"):
|
|
122
|
+
hacs_info = info_response.get("result", {})
|
|
123
|
+
disabled_reason = hacs_info.get("disabled_reason")
|
|
124
|
+
if disabled_reason:
|
|
125
|
+
return await add_timezone_metadata(
|
|
126
|
+
client,
|
|
127
|
+
{
|
|
128
|
+
"success": False,
|
|
129
|
+
"error": f"HACS is disabled: {disabled_reason}",
|
|
130
|
+
"error_code": "HACS_DISABLED",
|
|
131
|
+
"disabled_reason": disabled_reason,
|
|
132
|
+
"suggestions": [
|
|
133
|
+
"HACS requires a valid GitHub token to manage repositories",
|
|
134
|
+
"Configure a GitHub Personal Access Token in HACS settings",
|
|
135
|
+
"Ensure HACS has completed initial setup",
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Check if already installed by looking in the repository list
|
|
141
|
+
list_response = await ws_client.send_command("hacs/repositories/list")
|
|
142
|
+
if not list_response.get("success"):
|
|
143
|
+
return await add_timezone_metadata(
|
|
144
|
+
client,
|
|
145
|
+
{
|
|
146
|
+
"success": False,
|
|
147
|
+
"error": "Failed to get HACS repository list",
|
|
148
|
+
"error_code": "HACS_LIST_FAILED",
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
repos = list_response.get("result", [])
|
|
153
|
+
existing_repo = None
|
|
154
|
+
for repo in repos:
|
|
155
|
+
if repo.get("full_name", "").lower() == MCP_TOOLS_REPO.lower():
|
|
156
|
+
existing_repo = repo
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
# If already installed, return success
|
|
160
|
+
if existing_repo and existing_repo.get("installed"):
|
|
161
|
+
return await add_timezone_metadata(
|
|
162
|
+
client,
|
|
163
|
+
{
|
|
164
|
+
"success": True,
|
|
165
|
+
"already_installed": True,
|
|
166
|
+
"version": existing_repo.get("installed_version"),
|
|
167
|
+
"message": f"ha_mcp_tools is already installed (version {existing_repo.get('installed_version')})",
|
|
168
|
+
"services": [
|
|
169
|
+
"ha_mcp_tools.list_files - List files in allowed directories",
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# If repo not in HACS, add it first
|
|
175
|
+
if not existing_repo:
|
|
176
|
+
logger.info(f"Adding {MCP_TOOLS_REPO} to HACS")
|
|
177
|
+
hacs_category = CATEGORY_MAP.get("integration", "integration")
|
|
178
|
+
add_response = await ws_client.send_command(
|
|
179
|
+
"hacs/repositories/add",
|
|
180
|
+
repository=MCP_TOOLS_REPO,
|
|
181
|
+
category=hacs_category,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if not add_response.get("success"):
|
|
185
|
+
return await add_timezone_metadata(
|
|
186
|
+
client,
|
|
187
|
+
{
|
|
188
|
+
"success": False,
|
|
189
|
+
"error": f"Failed to add repository to HACS: {add_response}",
|
|
190
|
+
"error_code": "HACS_ADD_FAILED",
|
|
191
|
+
"suggestions": [
|
|
192
|
+
f"Verify the repository exists: https://github.com/{MCP_TOOLS_REPO}",
|
|
193
|
+
"Check HACS logs for errors",
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Get the new repo info
|
|
199
|
+
existing_repo = add_response.get("result", {})
|
|
200
|
+
|
|
201
|
+
# Now download/install the repository
|
|
202
|
+
repo_id = str(existing_repo.get("id")) if existing_repo else None
|
|
203
|
+
|
|
204
|
+
if not repo_id:
|
|
205
|
+
# HACS processes additions asynchronously, so poll for the repo to appear
|
|
206
|
+
import asyncio
|
|
207
|
+
max_attempts = 10
|
|
208
|
+
poll_interval = 1.0 # seconds
|
|
209
|
+
|
|
210
|
+
for attempt in range(max_attempts):
|
|
211
|
+
logger.debug(f"Polling for repository ID (attempt {attempt + 1}/{max_attempts})")
|
|
212
|
+
list_response = await ws_client.send_command("hacs/repositories/list")
|
|
213
|
+
repos = list_response.get("result", [])
|
|
214
|
+
for repo in repos:
|
|
215
|
+
if repo.get("full_name", "").lower() == MCP_TOOLS_REPO.lower():
|
|
216
|
+
repo_id = str(repo.get("id"))
|
|
217
|
+
logger.info(f"Found repository ID: {repo_id} after {attempt + 1} attempts")
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
if repo_id:
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
if attempt < max_attempts - 1:
|
|
224
|
+
await asyncio.sleep(poll_interval)
|
|
225
|
+
|
|
226
|
+
if not repo_id:
|
|
227
|
+
return await add_timezone_metadata(
|
|
228
|
+
client,
|
|
229
|
+
{
|
|
230
|
+
"success": False,
|
|
231
|
+
"error": "Could not find repository ID after adding (timed out after 10 attempts)",
|
|
232
|
+
"error_code": "HACS_REPO_ID_NOT_FOUND",
|
|
233
|
+
"suggestions": [
|
|
234
|
+
"HACS may be processing the request - try again in a few seconds",
|
|
235
|
+
"Check HACS logs for errors",
|
|
236
|
+
f"Verify the repository exists: https://github.com/{MCP_TOOLS_REPO}",
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
logger.info(f"Installing {MCP_TOOLS_REPO} (ID: {repo_id})")
|
|
242
|
+
download_response = await ws_client.send_command(
|
|
243
|
+
"hacs/repository/download",
|
|
244
|
+
repository=repo_id,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if not download_response.get("success"):
|
|
248
|
+
return await add_timezone_metadata(
|
|
249
|
+
client,
|
|
250
|
+
{
|
|
251
|
+
"success": False,
|
|
252
|
+
"error": f"Failed to download repository: {download_response}",
|
|
253
|
+
"error_code": "HACS_DOWNLOAD_FAILED",
|
|
254
|
+
"suggestions": [
|
|
255
|
+
"Check HACS logs for errors",
|
|
256
|
+
"Verify GitHub is accessible",
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
result = {
|
|
262
|
+
"success": True,
|
|
263
|
+
"installed": True,
|
|
264
|
+
"repository": MCP_TOOLS_REPO,
|
|
265
|
+
"message": "ha_mcp_tools installed successfully",
|
|
266
|
+
"services": [
|
|
267
|
+
"ha_mcp_tools.list_files - List files in allowed directories",
|
|
268
|
+
],
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Optionally restart Home Assistant
|
|
272
|
+
if restart:
|
|
273
|
+
try:
|
|
274
|
+
await client.call_service("homeassistant", "restart", {})
|
|
275
|
+
result["restarted"] = True
|
|
276
|
+
result["message"] += ". Home Assistant is restarting."
|
|
277
|
+
result["note"] = "Wait 1-5 minutes for Home Assistant to restart."
|
|
278
|
+
except Exception as restart_error:
|
|
279
|
+
# Connection errors during restart are expected
|
|
280
|
+
if "connection" in str(restart_error).lower():
|
|
281
|
+
result["restarted"] = True
|
|
282
|
+
result["message"] += ". Home Assistant is restarting."
|
|
283
|
+
result["note"] = (
|
|
284
|
+
"Wait 1-5 minutes for Home Assistant to restart."
|
|
285
|
+
)
|
|
286
|
+
else:
|
|
287
|
+
result["restart_error"] = str(restart_error)
|
|
288
|
+
result["message"] += ". Restart failed - please restart manually."
|
|
289
|
+
else:
|
|
290
|
+
result["note"] = "Restart Home Assistant for the integration to load."
|
|
291
|
+
|
|
292
|
+
return await add_timezone_metadata(client, result)
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
error_response = exception_to_structured_error(
|
|
296
|
+
e,
|
|
297
|
+
context={"tool": "ha_install_mcp_tools", "restart": restart},
|
|
298
|
+
)
|
|
299
|
+
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
300
|
+
error_response["error"]["suggestions"] = [
|
|
301
|
+
"Verify HACS is installed: https://hacs.xyz/",
|
|
302
|
+
"Check Home Assistant logs for errors",
|
|
303
|
+
"Ensure GitHub is accessible",
|
|
304
|
+
]
|
|
305
|
+
return await add_timezone_metadata(client, error_response)
|