ha-mcp-dev 7.1.0.dev306__tar.gz → 7.1.0.dev308__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.1.0.dev306/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.1.0.dev308}/PKG-INFO +1 -1
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/pyproject.toml +2 -1
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_addons.py +39 -40
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_dashboards.py +3 -4
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_entities.py +4 -4
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_resources.py +3 -4
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_search.py +15 -14
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/LICENSE +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/README.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/setup.cfg +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/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.1.0.
|
|
7
|
+
version = "7.1.0.dev308"
|
|
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"
|
|
@@ -177,6 +177,7 @@ dev = [
|
|
|
177
177
|
"lefthook>=1.10.0",
|
|
178
178
|
"ruff>=0.12.12",
|
|
179
179
|
"testcontainers>=4.13.0",
|
|
180
|
+
"ast-grep-cli>=0.42.0",
|
|
180
181
|
]
|
|
181
182
|
|
|
182
183
|
# Semantic versioning configuration
|
|
@@ -87,19 +87,19 @@ 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
|
-
|
|
90
|
+
raise_tool_error(create_error_response(
|
|
91
91
|
ErrorCode.RESOURCE_NOT_FOUND,
|
|
92
92
|
"Supervisor API not available",
|
|
93
93
|
details=str(result),
|
|
94
94
|
suggestions=[
|
|
95
95
|
"This feature requires Home Assistant OS or Supervised installation",
|
|
96
96
|
],
|
|
97
|
-
)
|
|
98
|
-
|
|
97
|
+
))
|
|
98
|
+
raise_tool_error(create_error_response(
|
|
99
99
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
100
100
|
f"Supervisor API call failed: {endpoint}",
|
|
101
101
|
details=str(result),
|
|
102
|
-
)
|
|
102
|
+
))
|
|
103
103
|
|
|
104
104
|
return {"success": True, "result": result.get("result", {})}
|
|
105
105
|
|
|
@@ -107,10 +107,9 @@ async def _supervisor_api_call(
|
|
|
107
107
|
raise
|
|
108
108
|
except Exception as e:
|
|
109
109
|
logger.error(f"Error calling Supervisor API {endpoint}: {e}")
|
|
110
|
-
|
|
110
|
+
exception_to_structured_error(
|
|
111
111
|
e,
|
|
112
112
|
context={"endpoint": endpoint},
|
|
113
|
-
raise_error=False,
|
|
114
113
|
suggestions=["Check Home Assistant connection and Supervisor availability"],
|
|
115
114
|
)
|
|
116
115
|
finally:
|
|
@@ -311,23 +310,23 @@ async def _call_addon_ws(
|
|
|
311
310
|
# 1. Sanitize path
|
|
312
311
|
normalized = unquote(path).lstrip("/")
|
|
313
312
|
if ".." in normalized.split("/"):
|
|
314
|
-
|
|
313
|
+
raise_tool_error(create_validation_error(
|
|
315
314
|
"Path contains '..' traversal component",
|
|
316
315
|
parameter="path",
|
|
317
316
|
details=f"Rejected path: {path}",
|
|
318
|
-
)
|
|
317
|
+
))
|
|
319
318
|
|
|
320
319
|
# 2. Get add-on info
|
|
321
320
|
addon_response = await get_addon_info(client, slug)
|
|
322
321
|
if not addon_response.get("success"):
|
|
323
|
-
|
|
322
|
+
raise_tool_error(addon_response)
|
|
324
323
|
|
|
325
324
|
addon = addon_response["addon"]
|
|
326
325
|
addon_name = addon.get("name", slug)
|
|
327
326
|
|
|
328
327
|
# 3. Verify add-on supports Ingress (unless using direct port override)
|
|
329
328
|
if not port and not addon.get("ingress"):
|
|
330
|
-
|
|
329
|
+
raise_tool_error(create_error_response(
|
|
331
330
|
ErrorCode.VALIDATION_FAILED,
|
|
332
331
|
f"Add-on '{addon_name}' does not support Ingress",
|
|
333
332
|
suggestions=[
|
|
@@ -335,37 +334,37 @@ async def _call_addon_ws(
|
|
|
335
334
|
f"Use ha_get_addon(slug='{slug}') to see available ports",
|
|
336
335
|
],
|
|
337
336
|
context={"slug": slug},
|
|
338
|
-
)
|
|
337
|
+
))
|
|
339
338
|
|
|
340
339
|
# 4. Verify add-on is running
|
|
341
340
|
if addon.get("state") != "started":
|
|
342
|
-
|
|
341
|
+
raise_tool_error(create_error_response(
|
|
343
342
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
344
343
|
f"Add-on '{addon_name}' is not running (state: {addon.get('state')})",
|
|
345
344
|
suggestions=[
|
|
346
345
|
f"Start the add-on first with: ha_call_service('hassio', 'addon_start', {{'addon': '{slug}'}})",
|
|
347
346
|
],
|
|
348
347
|
context={"slug": slug, "state": addon.get("state")},
|
|
349
|
-
)
|
|
348
|
+
))
|
|
350
349
|
|
|
351
350
|
# 5. Build WebSocket URL
|
|
352
351
|
addon_ip = addon.get("ip_address", "")
|
|
353
352
|
if port:
|
|
354
353
|
if not addon_ip:
|
|
355
|
-
|
|
354
|
+
raise_tool_error(create_error_response(
|
|
356
355
|
ErrorCode.INTERNAL_ERROR,
|
|
357
356
|
f"Add-on '{addon_name}' is missing ip_address",
|
|
358
357
|
context={"slug": slug},
|
|
359
|
-
)
|
|
358
|
+
))
|
|
360
359
|
target_port = port
|
|
361
360
|
else:
|
|
362
361
|
ingress_port = addon.get("ingress_port")
|
|
363
362
|
if not addon_ip or not ingress_port:
|
|
364
|
-
|
|
363
|
+
raise_tool_error(create_error_response(
|
|
365
364
|
ErrorCode.INTERNAL_ERROR,
|
|
366
365
|
f"Add-on '{addon_name}' is missing network info",
|
|
367
366
|
context={"slug": slug},
|
|
368
|
-
)
|
|
367
|
+
))
|
|
369
368
|
target_port = ingress_port
|
|
370
369
|
|
|
371
370
|
ws_url = f"ws://{addon_ip}:{target_port}/{normalized}"
|
|
@@ -439,7 +438,7 @@ async def _call_addon_ws(
|
|
|
439
438
|
total_size += len(clean)
|
|
440
439
|
|
|
441
440
|
except websockets.exceptions.InvalidHandshake as e:
|
|
442
|
-
|
|
441
|
+
raise_tool_error(create_error_response(
|
|
443
442
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
444
443
|
f"WebSocket handshake failed with '{addon_name}': {e!s}",
|
|
445
444
|
suggestions=[
|
|
@@ -447,9 +446,9 @@ async def _call_addon_ws(
|
|
|
447
446
|
f"Use ha_get_addon(slug='{slug}') to inspect available endpoints",
|
|
448
447
|
],
|
|
449
448
|
context={"slug": slug, "path": path},
|
|
450
|
-
)
|
|
449
|
+
))
|
|
451
450
|
except websockets.exceptions.ConnectionClosed as e:
|
|
452
|
-
|
|
451
|
+
raise_tool_error(create_error_response(
|
|
453
452
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
454
453
|
f"WebSocket connection to '{addon_name}' closed unexpectedly: {e!s}",
|
|
455
454
|
suggestions=[
|
|
@@ -457,20 +456,20 @@ async def _call_addon_ws(
|
|
|
457
456
|
"Try again or check add-on logs for errors",
|
|
458
457
|
],
|
|
459
458
|
context={"slug": slug, "path": path},
|
|
460
|
-
)
|
|
459
|
+
))
|
|
461
460
|
except TimeoutError:
|
|
462
|
-
|
|
461
|
+
raise_tool_error(create_timeout_error(
|
|
463
462
|
f"WebSocket connection to '{addon_name}'",
|
|
464
463
|
timeout,
|
|
465
464
|
details=f"path={path}",
|
|
466
465
|
context={"slug": slug, "path": path},
|
|
467
|
-
)
|
|
466
|
+
))
|
|
468
467
|
except OSError as e:
|
|
469
|
-
|
|
468
|
+
raise_tool_error(create_connection_error(
|
|
470
469
|
f"Failed to connect to add-on '{addon_name}' WebSocket: {e!s}",
|
|
471
470
|
details="Check that the add-on is running and the port is correct",
|
|
472
471
|
context={"slug": slug},
|
|
473
|
-
)
|
|
472
|
+
))
|
|
474
473
|
|
|
475
474
|
elapsed = round(time.monotonic() - start_time, 2)
|
|
476
475
|
|
|
@@ -554,23 +553,23 @@ async def _call_addon_api(
|
|
|
554
553
|
# 1. Sanitize path to prevent traversal attacks (including URL-encoded)
|
|
555
554
|
normalized = unquote(path).lstrip("/")
|
|
556
555
|
if ".." in normalized.split("/"):
|
|
557
|
-
|
|
556
|
+
raise_tool_error(create_validation_error(
|
|
558
557
|
"Path contains '..' traversal component",
|
|
559
558
|
parameter="path",
|
|
560
559
|
details=f"Rejected path: {path}",
|
|
561
|
-
)
|
|
560
|
+
))
|
|
562
561
|
|
|
563
562
|
# 2. Get add-on info to verify ingress support and get entry path
|
|
564
563
|
addon_response = await get_addon_info(client, slug)
|
|
565
564
|
if not addon_response.get("success"):
|
|
566
|
-
|
|
565
|
+
raise_tool_error(addon_response)
|
|
567
566
|
|
|
568
567
|
addon = addon_response["addon"]
|
|
569
568
|
addon_name = addon.get("name", slug)
|
|
570
569
|
|
|
571
570
|
# 3. Verify add-on supports Ingress (unless using direct port override)
|
|
572
571
|
if not port and not addon.get("ingress"):
|
|
573
|
-
|
|
572
|
+
raise_tool_error(create_error_response(
|
|
574
573
|
ErrorCode.VALIDATION_FAILED,
|
|
575
574
|
f"Add-on '{addon_name}' does not support Ingress",
|
|
576
575
|
suggestions=[
|
|
@@ -579,18 +578,18 @@ async def _call_addon_api(
|
|
|
579
578
|
"Use the 'port' parameter to connect to a direct access port",
|
|
580
579
|
],
|
|
581
580
|
context={"slug": slug},
|
|
582
|
-
)
|
|
581
|
+
))
|
|
583
582
|
|
|
584
583
|
# 4. Verify add-on is running
|
|
585
584
|
if addon.get("state") != "started":
|
|
586
|
-
|
|
585
|
+
raise_tool_error(create_error_response(
|
|
587
586
|
ErrorCode.SERVICE_CALL_FAILED,
|
|
588
587
|
f"Add-on '{addon_name}' is not running (state: {addon.get('state')})",
|
|
589
588
|
suggestions=[
|
|
590
589
|
f"Start the add-on first with: ha_call_service('hassio', 'addon_start', {{'addon': '{slug}'}})",
|
|
591
590
|
],
|
|
592
591
|
context={"slug": slug, "state": addon.get("state")},
|
|
593
|
-
)
|
|
592
|
+
))
|
|
594
593
|
|
|
595
594
|
# 5. Build URL to the add-on container
|
|
596
595
|
addon_ip = addon.get("ip_address", "")
|
|
@@ -600,21 +599,21 @@ async def _call_addon_api(
|
|
|
600
599
|
# (e.g., 1880 for Node-RED, 6052 for ESPHome) instead of the ingress port.
|
|
601
600
|
# Requires 'leave_front_door_open' or equivalent setting on the add-on.
|
|
602
601
|
if not addon_ip:
|
|
603
|
-
|
|
602
|
+
raise_tool_error(create_error_response(
|
|
604
603
|
ErrorCode.INTERNAL_ERROR,
|
|
605
604
|
f"Add-on '{addon_name}' is missing ip_address",
|
|
606
605
|
context={"slug": slug, "ip_address": addon_ip},
|
|
607
|
-
)
|
|
606
|
+
))
|
|
608
607
|
target_port = port
|
|
609
608
|
else:
|
|
610
609
|
# Default: use the ingress port for direct container communication
|
|
611
610
|
ingress_port = addon.get("ingress_port")
|
|
612
611
|
if not addon_ip or not ingress_port:
|
|
613
|
-
|
|
612
|
+
raise_tool_error(create_error_response(
|
|
614
613
|
ErrorCode.INTERNAL_ERROR,
|
|
615
614
|
f"Add-on '{addon_name}' is missing network info (ip_address or ingress_port)",
|
|
616
615
|
context={"slug": slug, "ip_address": addon_ip, "ingress_port": ingress_port},
|
|
617
|
-
)
|
|
616
|
+
))
|
|
618
617
|
target_port = ingress_port
|
|
619
618
|
|
|
620
619
|
url = f"http://{addon_ip}:{target_port}/{normalized}"
|
|
@@ -648,18 +647,18 @@ async def _call_addon_api(
|
|
|
648
647
|
content=request_content,
|
|
649
648
|
)
|
|
650
649
|
except httpx.TimeoutException:
|
|
651
|
-
|
|
650
|
+
raise_tool_error(create_timeout_error(
|
|
652
651
|
f"add-on API call to '{addon_name}'",
|
|
653
652
|
timeout,
|
|
654
653
|
details=f"path={path}, method={method}",
|
|
655
654
|
context={"slug": slug, "path": path},
|
|
656
|
-
)
|
|
655
|
+
))
|
|
657
656
|
except httpx.ConnectError as e:
|
|
658
|
-
|
|
657
|
+
raise_tool_error(create_connection_error(
|
|
659
658
|
f"Failed to connect to add-on '{addon_name}': {e!s}",
|
|
660
659
|
details="Check that the add-on is running and Home Assistant Ingress is working",
|
|
661
660
|
context={"slug": slug},
|
|
662
|
-
)
|
|
661
|
+
))
|
|
663
662
|
|
|
664
663
|
# 7. Parse response
|
|
665
664
|
content_type = response.headers.get("content-type", "")
|
{ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
@@ -1057,14 +1057,14 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1057
1057
|
break
|
|
1058
1058
|
|
|
1059
1059
|
if resolved_id is None:
|
|
1060
|
-
|
|
1060
|
+
raise_tool_error(create_resource_not_found_error(
|
|
1061
1061
|
"Dashboard",
|
|
1062
1062
|
dashboard_id,
|
|
1063
1063
|
details=(
|
|
1064
1064
|
f"No dashboard found with ID or URL path '{dashboard_id}'. "
|
|
1065
1065
|
"Use ha_config_get_dashboard(list_only=True) to see available dashboards."
|
|
1066
1066
|
),
|
|
1067
|
-
)
|
|
1067
|
+
))
|
|
1068
1068
|
|
|
1069
1069
|
response = await client.send_websocket_message(
|
|
1070
1070
|
{"type": "lovelace/dashboards/delete", "dashboard_id": resolved_id}
|
|
@@ -1121,10 +1121,9 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1121
1121
|
raise
|
|
1122
1122
|
except Exception as e:
|
|
1123
1123
|
logger.error(f"Error deleting dashboard: {e}")
|
|
1124
|
-
|
|
1124
|
+
exception_to_structured_error(
|
|
1125
1125
|
e,
|
|
1126
1126
|
context={"action": "delete", "dashboard_id": dashboard_id},
|
|
1127
|
-
raise_error=False,
|
|
1128
1127
|
suggestions=[
|
|
1129
1128
|
"Verify dashboard exists and is storage-mode",
|
|
1130
1129
|
"Check that you have admin permissions",
|
|
@@ -117,10 +117,10 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
117
117
|
try:
|
|
118
118
|
enabled_bool = coerce_bool_param(enabled, "enabled")
|
|
119
119
|
except ValueError as e:
|
|
120
|
-
|
|
120
|
+
raise_tool_error(create_error_response(
|
|
121
121
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
122
122
|
str(e),
|
|
123
|
-
)
|
|
123
|
+
))
|
|
124
124
|
message["disabled_by"] = None if enabled_bool else "user"
|
|
125
125
|
updates_made.append("enabled" if enabled_bool else "disabled")
|
|
126
126
|
|
|
@@ -128,10 +128,10 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
128
128
|
try:
|
|
129
129
|
hidden_bool = coerce_bool_param(hidden, "hidden")
|
|
130
130
|
except ValueError as e:
|
|
131
|
-
|
|
131
|
+
raise_tool_error(create_error_response(
|
|
132
132
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
133
133
|
str(e),
|
|
134
|
-
)
|
|
134
|
+
))
|
|
135
135
|
message["hidden_by"] = "user" if hidden_bool else None
|
|
136
136
|
updates_made.append("hidden" if hidden_bool else "visible")
|
|
137
137
|
|
|
@@ -544,14 +544,14 @@ def register_resources_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
544
544
|
error_str = str(error_msg)
|
|
545
545
|
|
|
546
546
|
if "not found" in error_str.lower() or "unable to find" in error_str.lower():
|
|
547
|
-
|
|
547
|
+
raise_tool_error(create_resource_not_found_error(
|
|
548
548
|
"Dashboard resource",
|
|
549
549
|
resource_id,
|
|
550
550
|
details=(
|
|
551
551
|
f"Resource '{resource_id}' not found. "
|
|
552
552
|
"Use ha_config_list_dashboard_resources() to see available resources."
|
|
553
553
|
),
|
|
554
|
-
)
|
|
554
|
+
))
|
|
555
555
|
|
|
556
556
|
raise_tool_error(create_error_response(
|
|
557
557
|
code=ErrorCode.SERVICE_CALL_FAILED,
|
|
@@ -575,10 +575,9 @@ def register_resources_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
575
575
|
raise
|
|
576
576
|
except Exception as e:
|
|
577
577
|
logger.error(f"Error deleting dashboard resource: {e}")
|
|
578
|
-
|
|
578
|
+
exception_to_structured_error(
|
|
579
579
|
e,
|
|
580
580
|
context={"action": "delete", "resource_id": resource_id},
|
|
581
|
-
raise_error=False,
|
|
582
581
|
suggestions=[
|
|
583
582
|
"Verify resource ID using ha_config_list_dashboard_resources()",
|
|
584
583
|
"Check that you have admin permissions",
|
|
@@ -8,12 +8,13 @@ import asyncio
|
|
|
8
8
|
import logging
|
|
9
9
|
from typing import Annotated, Any, Literal, cast
|
|
10
10
|
|
|
11
|
+
from fastmcp.exceptions import ToolError
|
|
11
12
|
from pydantic import Field
|
|
12
13
|
|
|
13
14
|
from ..config import get_global_settings
|
|
14
15
|
from ..errors import create_validation_error
|
|
15
16
|
from ..transforms.categorized_search import DEFAULT_PINNED_TOOLS
|
|
16
|
-
from .helpers import exception_to_structured_error, log_tool_usage
|
|
17
|
+
from .helpers import exception_to_structured_error, log_tool_usage
|
|
17
18
|
from .util_helpers import (
|
|
18
19
|
add_timezone_metadata,
|
|
19
20
|
coerce_bool_param,
|
|
@@ -475,23 +476,22 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
475
476
|
|
|
476
477
|
return await add_timezone_metadata(client, result)
|
|
477
478
|
|
|
479
|
+
except ToolError:
|
|
480
|
+
raise
|
|
478
481
|
except Exception as e:
|
|
479
|
-
|
|
482
|
+
exception_to_structured_error(
|
|
480
483
|
e,
|
|
481
484
|
context={
|
|
482
485
|
"query": query,
|
|
483
486
|
"domain_filter": domain_filter,
|
|
484
487
|
"area_filter": area_filter,
|
|
485
488
|
},
|
|
486
|
-
raise_error=False,
|
|
487
489
|
suggestions=[
|
|
488
490
|
"Check Home Assistant connection",
|
|
489
491
|
"Try simpler search terms",
|
|
490
492
|
"Check area/domain filter spelling",
|
|
491
493
|
],
|
|
492
494
|
)
|
|
493
|
-
error_with_tz = await add_timezone_metadata(client, error_response)
|
|
494
|
-
raise_tool_error(error_with_tz)
|
|
495
495
|
|
|
496
496
|
@mcp.tool(
|
|
497
497
|
tags={"Search & Discovery"},
|
|
@@ -737,6 +737,8 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
737
737
|
exact_match=exact_match_bool,
|
|
738
738
|
)
|
|
739
739
|
return cast(dict[str, Any], result)
|
|
740
|
+
except ToolError:
|
|
741
|
+
raise
|
|
740
742
|
except Exception as e:
|
|
741
743
|
logger.error(
|
|
742
744
|
f"Error in deep search: query={query}, "
|
|
@@ -744,14 +746,13 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
744
746
|
f"error={e}",
|
|
745
747
|
exc_info=True,
|
|
746
748
|
)
|
|
747
|
-
|
|
749
|
+
exception_to_structured_error(
|
|
748
750
|
e,
|
|
749
751
|
context={
|
|
750
752
|
"query": query,
|
|
751
753
|
"search_types": parsed_search_types,
|
|
752
754
|
"limit": limit,
|
|
753
755
|
},
|
|
754
|
-
raise_error=False,
|
|
755
756
|
suggestions=[
|
|
756
757
|
"Check Home Assistant connection",
|
|
757
758
|
"Try simpler search terms",
|
|
@@ -772,19 +773,18 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
772
773
|
try:
|
|
773
774
|
result = await client.get_entity_state(entity_id)
|
|
774
775
|
return await add_timezone_metadata(client, result)
|
|
776
|
+
except ToolError:
|
|
777
|
+
raise
|
|
775
778
|
except Exception as e:
|
|
776
|
-
|
|
779
|
+
exception_to_structured_error(
|
|
777
780
|
e,
|
|
778
781
|
context={"entity_id": entity_id},
|
|
779
|
-
raise_error=False,
|
|
780
782
|
suggestions=[
|
|
781
783
|
f"Verify entity '{entity_id}' exists in Home Assistant",
|
|
782
784
|
"Check Home Assistant connection",
|
|
783
785
|
"Use ha_search_entities() to find correct entity IDs",
|
|
784
786
|
],
|
|
785
787
|
)
|
|
786
|
-
error_with_tz = await add_timezone_metadata(client, error_response)
|
|
787
|
-
raise_tool_error(error_with_tz)
|
|
788
788
|
|
|
789
789
|
@mcp.tool(
|
|
790
790
|
tags={"Search & Discovery"},
|
|
@@ -867,6 +867,7 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
867
867
|
return {"success": True, "entity_id": entity_id, "state": state}
|
|
868
868
|
except Exception as e:
|
|
869
869
|
logger.warning(f"Failed to fetch state for '{entity_id}': {e}")
|
|
870
|
+
# ast-grep-ignore — batch item failure, aggregated via asyncio.gather
|
|
870
871
|
return exception_to_structured_error(
|
|
871
872
|
e,
|
|
872
873
|
context={"entity_id": entity_id},
|
|
@@ -913,11 +914,11 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
913
914
|
|
|
914
915
|
return await add_timezone_metadata(client, response)
|
|
915
916
|
|
|
917
|
+
except ToolError:
|
|
918
|
+
raise
|
|
916
919
|
except Exception as e:
|
|
917
920
|
logger.error(f"Error getting bulk states: {e}", exc_info=True)
|
|
918
|
-
|
|
921
|
+
exception_to_structured_error(
|
|
919
922
|
e,
|
|
920
923
|
context={"entity_ids": entity_ids},
|
|
921
|
-
raise_error=False,
|
|
922
924
|
)
|
|
923
|
-
return await add_timezone_metadata(client, error_response)
|
|
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.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/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.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/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.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/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
|
{ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/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.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/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
|