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.
Files changed (99) hide show
  1. {ha_mcp_dev-7.1.0.dev306/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.1.0.dev308}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/pyproject.toml +2 -1
  3. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_addons.py +39 -40
  4. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_dashboards.py +3 -4
  5. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_entities.py +4 -4
  6. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_resources.py +3 -4
  7. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_search.py +15 -14
  8. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/LICENSE +0 -0
  10. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/README.md +0 -0
  12. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/auth/__init__.py +0 -0
  17. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/auth/consent_form.py +0 -0
  18. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/auth/provider.py +0 -0
  19. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/client/__init__.py +0 -0
  20. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/client/rest_client.py +0 -0
  21. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/client/websocket_client.py +0 -0
  22. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/client/websocket_listener.py +0 -0
  23. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/config.py +0 -0
  24. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/errors.py +0 -0
  25. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/py.typed +0 -0
  26. {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
  27. {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
  28. {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
  29. {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
  30. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  31. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  32. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  33. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  34. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  35. {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
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {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
  43. {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
  44. {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
  45. {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
  46. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/server.py +0 -0
  47. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/smoke_test.py +0 -0
  48. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/__init__.py +0 -0
  49. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/backup.py +0 -0
  50. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  51. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/device_control.py +0 -0
  52. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/enhanced.py +0 -0
  53. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/helpers.py +0 -0
  54. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/registry.py +0 -0
  55. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/smart_search.py +0 -0
  56. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_areas.py +0 -0
  57. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  58. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  59. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_calendar.py +0 -0
  60. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_camera.py +0 -0
  61. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_categories.py +0 -0
  62. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  63. {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
  64. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  65. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  66. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  67. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_groups.py +0 -0
  68. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_hacs.py +0 -0
  69. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_history.py +0 -0
  70. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_integrations.py +0 -0
  71. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_labels.py +0 -0
  72. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  73. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_registry.py +0 -0
  74. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_service.py +0 -0
  75. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_services.py +0 -0
  76. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_system.py +0 -0
  77. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_todo.py +0 -0
  78. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_traces.py +0 -0
  79. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_updates.py +0 -0
  80. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_utility.py +0 -0
  81. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  82. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/tools_zones.py +0 -0
  83. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/tools/util_helpers.py +0 -0
  84. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/transforms/__init__.py +0 -0
  85. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/transforms/categorized_search.py +0 -0
  86. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/__init__.py +0 -0
  87. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/domain_handlers.py +0 -0
  88. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  89. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/operation_manager.py +0 -0
  90. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/python_sandbox.py +0 -0
  91. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp/utils/usage_logger.py +0 -0
  92. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  93. {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
  94. {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
  95. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  96. {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
  97. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/tests/__init__.py +0 -0
  98. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/tests/test_constants.py +0 -0
  99. {ha_mcp_dev-7.1.0.dev306 → ha_mcp_dev-7.1.0.dev308}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.1.0.dev306
3
+ Version: 7.1.0.dev308
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -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.dev306"
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
- return create_error_response(
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
- return create_error_response(
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
- return exception_to_structured_error(
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
- return create_validation_error(
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
- return addon_response
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
- return create_error_response(
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
- return create_error_response(
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
- return create_error_response(
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
- return create_error_response(
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
- return create_error_response(
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
- return create_error_response(
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
- return create_timeout_error(
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
- return create_connection_error(
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
- return create_validation_error(
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
- return addon_response
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
- return create_error_response(
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
- return create_error_response(
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
- return create_error_response(
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
- return create_error_response(
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
- return create_timeout_error(
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
- return create_connection_error(
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", "")
@@ -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
- return create_resource_not_found_error(
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
- return exception_to_structured_error(
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
- return create_error_response(
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
- return create_error_response(
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
- return create_resource_not_found_error(
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
- return exception_to_structured_error(
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, raise_tool_error
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
- error_response = exception_to_structured_error(
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
- return exception_to_structured_error(
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
- error_response = exception_to_structured_error(
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
- error_response = exception_to_structured_error(
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.1.0.dev306
3
+ Version: 7.1.0.dev308
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT