ha-mcp-dev 7.2.0.dev327__tar.gz → 7.2.0.dev329__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.dev327/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev329}/PKG-INFO +1 -1
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/pyproject.toml +1 -1
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_addons.py +206 -138
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/README.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/setup.cfg +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/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.dev329"
|
|
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"
|
|
@@ -87,19 +87,23 @@ async def _supervisor_api_call(
|
|
|
87
87
|
if not result.get("success"):
|
|
88
88
|
error_msg = str(result.get("error", ""))
|
|
89
89
|
if "not_found" in error_msg.lower() or "unknown" in error_msg.lower():
|
|
90
|
-
raise_tool_error(
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
raise_tool_error(
|
|
91
|
+
create_error_response(
|
|
92
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
93
|
+
"Supervisor API not available",
|
|
94
|
+
details=str(result),
|
|
95
|
+
suggestions=[
|
|
96
|
+
"This feature requires Home Assistant OS or Supervised installation",
|
|
97
|
+
],
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
raise_tool_error(
|
|
101
|
+
create_error_response(
|
|
102
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
103
|
+
f"Supervisor API call failed: {endpoint}",
|
|
93
104
|
details=str(result),
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
],
|
|
97
|
-
))
|
|
98
|
-
raise_tool_error(create_error_response(
|
|
99
|
-
ErrorCode.SERVICE_CALL_FAILED,
|
|
100
|
-
f"Supervisor API call failed: {endpoint}",
|
|
101
|
-
details=str(result),
|
|
102
|
-
))
|
|
105
|
+
)
|
|
106
|
+
)
|
|
103
107
|
|
|
104
108
|
return {"success": True, "result": result.get("result", {})}
|
|
105
109
|
|
|
@@ -120,9 +124,7 @@ async def _supervisor_api_call(
|
|
|
120
124
|
pass
|
|
121
125
|
|
|
122
126
|
|
|
123
|
-
async def get_addon_info(
|
|
124
|
-
client: HomeAssistantClient, slug: str
|
|
125
|
-
) -> dict[str, Any]:
|
|
127
|
+
async def get_addon_info(client: HomeAssistantClient, slug: str) -> dict[str, Any]:
|
|
126
128
|
"""Get detailed info for a specific add-on.
|
|
127
129
|
|
|
128
130
|
Args:
|
|
@@ -157,6 +159,29 @@ async def list_addons(
|
|
|
157
159
|
data = response["result"]
|
|
158
160
|
addons = data.get("addons", [])
|
|
159
161
|
|
|
162
|
+
# Fetch stats for running addons in parallel to avoid sequential overhead
|
|
163
|
+
stats_by_slug: dict[str, dict[str, Any] | None] = {}
|
|
164
|
+
if include_stats:
|
|
165
|
+
running_slugs = [a.get("slug") for a in addons if a.get("state") == "started"]
|
|
166
|
+
|
|
167
|
+
async def _fetch_stats(slug: str) -> tuple[str, dict[str, Any] | None]:
|
|
168
|
+
try:
|
|
169
|
+
resp = await _supervisor_api_call(client, f"/addons/{slug}/stats")
|
|
170
|
+
if resp.get("success"):
|
|
171
|
+
s = resp["result"]
|
|
172
|
+
return slug, {
|
|
173
|
+
"cpu_percent": s.get("cpu_percent"),
|
|
174
|
+
"memory_percent": s.get("memory_percent"),
|
|
175
|
+
"memory_usage": s.get("memory_usage"),
|
|
176
|
+
"memory_limit": s.get("memory_limit"),
|
|
177
|
+
}
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
logger.warning("Failed to fetch stats for addon %s: %s", slug, exc)
|
|
180
|
+
return slug, None
|
|
181
|
+
|
|
182
|
+
results = await asyncio.gather(*[_fetch_stats(slug) for slug in running_slugs])
|
|
183
|
+
stats_by_slug = dict(results)
|
|
184
|
+
|
|
160
185
|
# Format add-on information
|
|
161
186
|
formatted_addons = []
|
|
162
187
|
for addon in addons:
|
|
@@ -171,14 +196,8 @@ async def list_addons(
|
|
|
171
196
|
"repository": addon.get("repository"),
|
|
172
197
|
}
|
|
173
198
|
|
|
174
|
-
# Include stats if requested
|
|
175
199
|
if include_stats:
|
|
176
|
-
addon_info["stats"] =
|
|
177
|
-
"cpu_percent": addon.get("cpu_percent"),
|
|
178
|
-
"memory_percent": addon.get("memory_percent"),
|
|
179
|
-
"memory_usage": addon.get("memory_usage"),
|
|
180
|
-
"memory_limit": addon.get("memory_limit"),
|
|
181
|
-
}
|
|
200
|
+
addon_info["stats"] = stats_by_slug.get(addon.get("slug"))
|
|
182
201
|
|
|
183
202
|
formatted_addons.append(addon_info)
|
|
184
203
|
|
|
@@ -310,11 +329,13 @@ async def _call_addon_ws(
|
|
|
310
329
|
# 1. Sanitize path
|
|
311
330
|
normalized = unquote(path).lstrip("/")
|
|
312
331
|
if ".." in normalized.split("/"):
|
|
313
|
-
raise_tool_error(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
332
|
+
raise_tool_error(
|
|
333
|
+
create_validation_error(
|
|
334
|
+
"Path contains '..' traversal component",
|
|
335
|
+
parameter="path",
|
|
336
|
+
details=f"Rejected path: {path}",
|
|
337
|
+
)
|
|
338
|
+
)
|
|
318
339
|
|
|
319
340
|
# 2. Get add-on info
|
|
320
341
|
addon_response = await get_addon_info(client, slug)
|
|
@@ -326,45 +347,53 @@ async def _call_addon_ws(
|
|
|
326
347
|
|
|
327
348
|
# 3. Verify add-on supports Ingress (unless using direct port override)
|
|
328
349
|
if not port and not addon.get("ingress"):
|
|
329
|
-
raise_tool_error(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
350
|
+
raise_tool_error(
|
|
351
|
+
create_error_response(
|
|
352
|
+
ErrorCode.VALIDATION_FAILED,
|
|
353
|
+
f"Add-on '{addon_name}' does not support Ingress",
|
|
354
|
+
suggestions=[
|
|
355
|
+
"Use the 'port' parameter for WebSocket connections to this add-on",
|
|
356
|
+
f"Use ha_get_addon(slug='{slug}') to see available ports",
|
|
357
|
+
],
|
|
358
|
+
context={"slug": slug},
|
|
359
|
+
)
|
|
360
|
+
)
|
|
338
361
|
|
|
339
362
|
# 4. Verify add-on is running
|
|
340
363
|
if addon.get("state") != "started":
|
|
341
|
-
raise_tool_error(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
364
|
+
raise_tool_error(
|
|
365
|
+
create_error_response(
|
|
366
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
367
|
+
f"Add-on '{addon_name}' is not running (state: {addon.get('state')})",
|
|
368
|
+
suggestions=[
|
|
369
|
+
f"Start the add-on first with: ha_call_service('hassio', 'addon_start', {{'addon': '{slug}'}})",
|
|
370
|
+
],
|
|
371
|
+
context={"slug": slug, "state": addon.get("state")},
|
|
372
|
+
)
|
|
373
|
+
)
|
|
349
374
|
|
|
350
375
|
# 5. Build WebSocket URL
|
|
351
376
|
addon_ip = addon.get("ip_address", "")
|
|
352
377
|
if port:
|
|
353
378
|
if not addon_ip:
|
|
354
|
-
raise_tool_error(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
379
|
+
raise_tool_error(
|
|
380
|
+
create_error_response(
|
|
381
|
+
ErrorCode.INTERNAL_ERROR,
|
|
382
|
+
f"Add-on '{addon_name}' is missing ip_address",
|
|
383
|
+
context={"slug": slug},
|
|
384
|
+
)
|
|
385
|
+
)
|
|
359
386
|
target_port = port
|
|
360
387
|
else:
|
|
361
388
|
ingress_port = addon.get("ingress_port")
|
|
362
389
|
if not addon_ip or not ingress_port:
|
|
363
|
-
raise_tool_error(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
390
|
+
raise_tool_error(
|
|
391
|
+
create_error_response(
|
|
392
|
+
ErrorCode.INTERNAL_ERROR,
|
|
393
|
+
f"Add-on '{addon_name}' is missing network info",
|
|
394
|
+
context={"slug": slug},
|
|
395
|
+
)
|
|
396
|
+
)
|
|
368
397
|
target_port = ingress_port
|
|
369
398
|
|
|
370
399
|
ws_url = f"ws://{addon_ip}:{target_port}/{normalized}"
|
|
@@ -438,38 +467,46 @@ async def _call_addon_ws(
|
|
|
438
467
|
total_size += len(clean)
|
|
439
468
|
|
|
440
469
|
except websockets.exceptions.InvalidHandshake as e:
|
|
441
|
-
raise_tool_error(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
470
|
+
raise_tool_error(
|
|
471
|
+
create_error_response(
|
|
472
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
473
|
+
f"WebSocket handshake failed with '{addon_name}': {e!s}",
|
|
474
|
+
suggestions=[
|
|
475
|
+
"Check that the add-on supports WebSocket on this path",
|
|
476
|
+
f"Use ha_get_addon(slug='{slug}') to inspect available endpoints",
|
|
477
|
+
],
|
|
478
|
+
context={"slug": slug, "path": path},
|
|
479
|
+
)
|
|
480
|
+
)
|
|
450
481
|
except websockets.exceptions.ConnectionClosed as e:
|
|
451
|
-
raise_tool_error(
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
482
|
+
raise_tool_error(
|
|
483
|
+
create_error_response(
|
|
484
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
485
|
+
f"WebSocket connection to '{addon_name}' closed unexpectedly: {e!s}",
|
|
486
|
+
suggestions=[
|
|
487
|
+
"The add-on may have rejected the connection or restarted",
|
|
488
|
+
"Try again or check add-on logs for errors",
|
|
489
|
+
],
|
|
490
|
+
context={"slug": slug, "path": path},
|
|
491
|
+
)
|
|
492
|
+
)
|
|
460
493
|
except TimeoutError:
|
|
461
|
-
raise_tool_error(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
494
|
+
raise_tool_error(
|
|
495
|
+
create_timeout_error(
|
|
496
|
+
f"WebSocket connection to '{addon_name}'",
|
|
497
|
+
timeout,
|
|
498
|
+
details=f"path={path}",
|
|
499
|
+
context={"slug": slug, "path": path},
|
|
500
|
+
)
|
|
501
|
+
)
|
|
467
502
|
except OSError as e:
|
|
468
|
-
raise_tool_error(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
503
|
+
raise_tool_error(
|
|
504
|
+
create_connection_error(
|
|
505
|
+
f"Failed to connect to add-on '{addon_name}' WebSocket: {e!s}",
|
|
506
|
+
details="Check that the add-on is running and the port is correct",
|
|
507
|
+
context={"slug": slug},
|
|
508
|
+
)
|
|
509
|
+
)
|
|
473
510
|
|
|
474
511
|
elapsed = round(time.monotonic() - start_time, 2)
|
|
475
512
|
|
|
@@ -553,11 +590,13 @@ async def _call_addon_api(
|
|
|
553
590
|
# 1. Sanitize path to prevent traversal attacks (including URL-encoded)
|
|
554
591
|
normalized = unquote(path).lstrip("/")
|
|
555
592
|
if ".." in normalized.split("/"):
|
|
556
|
-
raise_tool_error(
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
593
|
+
raise_tool_error(
|
|
594
|
+
create_validation_error(
|
|
595
|
+
"Path contains '..' traversal component",
|
|
596
|
+
parameter="path",
|
|
597
|
+
details=f"Rejected path: {path}",
|
|
598
|
+
)
|
|
599
|
+
)
|
|
561
600
|
|
|
562
601
|
# 2. Get add-on info to verify ingress support and get entry path
|
|
563
602
|
addon_response = await get_addon_info(client, slug)
|
|
@@ -569,27 +608,31 @@ async def _call_addon_api(
|
|
|
569
608
|
|
|
570
609
|
# 3. Verify add-on supports Ingress (unless using direct port override)
|
|
571
610
|
if not port and not addon.get("ingress"):
|
|
572
|
-
raise_tool_error(
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
611
|
+
raise_tool_error(
|
|
612
|
+
create_error_response(
|
|
613
|
+
ErrorCode.VALIDATION_FAILED,
|
|
614
|
+
f"Add-on '{addon_name}' does not support Ingress",
|
|
615
|
+
suggestions=[
|
|
616
|
+
"Check if this add-on exposes a direct port instead",
|
|
617
|
+
f"Use ha_get_addon(slug='{slug}') to see port mappings",
|
|
618
|
+
"Use the 'port' parameter to connect to a direct access port",
|
|
619
|
+
],
|
|
620
|
+
context={"slug": slug},
|
|
621
|
+
)
|
|
622
|
+
)
|
|
582
623
|
|
|
583
624
|
# 4. Verify add-on is running
|
|
584
625
|
if addon.get("state") != "started":
|
|
585
|
-
raise_tool_error(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
626
|
+
raise_tool_error(
|
|
627
|
+
create_error_response(
|
|
628
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
629
|
+
f"Add-on '{addon_name}' is not running (state: {addon.get('state')})",
|
|
630
|
+
suggestions=[
|
|
631
|
+
f"Start the add-on first with: ha_call_service('hassio', 'addon_start', {{'addon': '{slug}'}})",
|
|
632
|
+
],
|
|
633
|
+
context={"slug": slug, "state": addon.get("state")},
|
|
634
|
+
)
|
|
635
|
+
)
|
|
593
636
|
|
|
594
637
|
# 5. Build URL to the add-on container
|
|
595
638
|
addon_ip = addon.get("ip_address", "")
|
|
@@ -599,21 +642,29 @@ async def _call_addon_api(
|
|
|
599
642
|
# (e.g., 1880 for Node-RED, 6052 for ESPHome) instead of the ingress port.
|
|
600
643
|
# Requires 'leave_front_door_open' or equivalent setting on the add-on.
|
|
601
644
|
if not addon_ip:
|
|
602
|
-
raise_tool_error(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
645
|
+
raise_tool_error(
|
|
646
|
+
create_error_response(
|
|
647
|
+
ErrorCode.INTERNAL_ERROR,
|
|
648
|
+
f"Add-on '{addon_name}' is missing ip_address",
|
|
649
|
+
context={"slug": slug, "ip_address": addon_ip},
|
|
650
|
+
)
|
|
651
|
+
)
|
|
607
652
|
target_port = port
|
|
608
653
|
else:
|
|
609
654
|
# Default: use the ingress port for direct container communication
|
|
610
655
|
ingress_port = addon.get("ingress_port")
|
|
611
656
|
if not addon_ip or not ingress_port:
|
|
612
|
-
raise_tool_error(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
657
|
+
raise_tool_error(
|
|
658
|
+
create_error_response(
|
|
659
|
+
ErrorCode.INTERNAL_ERROR,
|
|
660
|
+
f"Add-on '{addon_name}' is missing network info (ip_address or ingress_port)",
|
|
661
|
+
context={
|
|
662
|
+
"slug": slug,
|
|
663
|
+
"ip_address": addon_ip,
|
|
664
|
+
"ingress_port": ingress_port,
|
|
665
|
+
},
|
|
666
|
+
)
|
|
667
|
+
)
|
|
617
668
|
target_port = ingress_port
|
|
618
669
|
|
|
619
670
|
url = f"http://{addon_ip}:{target_port}/{normalized}"
|
|
@@ -647,18 +698,22 @@ async def _call_addon_api(
|
|
|
647
698
|
content=request_content,
|
|
648
699
|
)
|
|
649
700
|
except httpx.TimeoutException:
|
|
650
|
-
raise_tool_error(
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
701
|
+
raise_tool_error(
|
|
702
|
+
create_timeout_error(
|
|
703
|
+
f"add-on API call to '{addon_name}'",
|
|
704
|
+
timeout,
|
|
705
|
+
details=f"path={path}, method={method}",
|
|
706
|
+
context={"slug": slug, "path": path},
|
|
707
|
+
)
|
|
708
|
+
)
|
|
656
709
|
except httpx.ConnectError as e:
|
|
657
|
-
raise_tool_error(
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
710
|
+
raise_tool_error(
|
|
711
|
+
create_connection_error(
|
|
712
|
+
f"Failed to connect to add-on '{addon_name}': {e!s}",
|
|
713
|
+
details="Check that the add-on is running and Home Assistant Ingress is working",
|
|
714
|
+
context={"slug": slug},
|
|
715
|
+
)
|
|
716
|
+
)
|
|
662
717
|
|
|
663
718
|
# 7. Parse response
|
|
664
719
|
content_type = response.headers.get("content-type", "")
|
|
@@ -745,7 +800,9 @@ async def _call_addon_api(
|
|
|
745
800
|
|
|
746
801
|
if truncated:
|
|
747
802
|
result["truncated"] = True
|
|
748
|
-
result["note"] =
|
|
803
|
+
result["note"] = (
|
|
804
|
+
f"Response truncated to {_MAX_RESPONSE_SIZE // 1024}KB. The full response was larger."
|
|
805
|
+
)
|
|
749
806
|
|
|
750
807
|
if response.status_code >= 400:
|
|
751
808
|
result["error"] = f"Add-on API returned HTTP {response.status_code}"
|
|
@@ -782,7 +839,14 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
782
839
|
**kwargs: Additional arguments (ignored, for auto-discovery compatibility)
|
|
783
840
|
"""
|
|
784
841
|
|
|
785
|
-
@mcp.tool(
|
|
842
|
+
@mcp.tool(
|
|
843
|
+
tags={"Add-ons"},
|
|
844
|
+
annotations={
|
|
845
|
+
"idempotentHint": True,
|
|
846
|
+
"readOnlyHint": True,
|
|
847
|
+
"title": "Get Add-ons",
|
|
848
|
+
},
|
|
849
|
+
)
|
|
786
850
|
@log_tool_usage
|
|
787
851
|
async def ha_get_addon(
|
|
788
852
|
source: Annotated[
|
|
@@ -867,11 +931,13 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
867
931
|
elif effective_source == "installed":
|
|
868
932
|
result = await list_addons(client, include_stats)
|
|
869
933
|
else:
|
|
870
|
-
raise_tool_error(
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
934
|
+
raise_tool_error(
|
|
935
|
+
create_validation_error(
|
|
936
|
+
f"Invalid source: {source}. Must be 'installed' or 'available'.",
|
|
937
|
+
parameter="source",
|
|
938
|
+
details="Valid sources: installed, available",
|
|
939
|
+
)
|
|
940
|
+
)
|
|
875
941
|
|
|
876
942
|
if not result.get("success"):
|
|
877
943
|
raise_tool_error(result)
|
|
@@ -993,10 +1059,12 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
993
1059
|
# HTTP mode
|
|
994
1060
|
valid_methods = {"GET", "POST", "PUT", "DELETE", "PATCH"}
|
|
995
1061
|
if method.upper() not in valid_methods:
|
|
996
|
-
raise_tool_error(
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1062
|
+
raise_tool_error(
|
|
1063
|
+
create_validation_error(
|
|
1064
|
+
f"Invalid HTTP method: {method}. Must be one of: {', '.join(sorted(valid_methods))}",
|
|
1065
|
+
parameter="method",
|
|
1066
|
+
)
|
|
1067
|
+
)
|
|
1000
1068
|
|
|
1001
1069
|
result = await _call_addon_api(
|
|
1002
1070
|
client=client,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|