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.
Files changed (100) hide show
  1. {ha_mcp_dev-7.2.0.dev327/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev329}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_addons.py +206 -138
  4. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  5. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/LICENSE +0 -0
  6. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/MANIFEST.in +0 -0
  7. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/README.md +0 -0
  8. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/setup.cfg +0 -0
  9. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/__init__.py +0 -0
  10. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/__main__.py +0 -0
  11. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/_pypi_marker +0 -0
  12. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/auth/__init__.py +0 -0
  13. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/auth/consent_form.py +0 -0
  14. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/auth/provider.py +0 -0
  15. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/client/__init__.py +0 -0
  16. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/client/rest_client.py +0 -0
  17. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/client/websocket_client.py +0 -0
  18. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/client/websocket_listener.py +0 -0
  19. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/config.py +0 -0
  20. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/errors.py +0 -0
  21. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/py.typed +0 -0
  22. {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
  23. {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
  24. {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
  25. {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
  26. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  27. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  28. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  29. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  30. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  31. {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
  32. {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
  33. {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
  34. {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
  35. {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
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/server.py +0 -0
  43. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/smoke_test.py +0 -0
  44. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/__init__.py +0 -0
  45. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/backup.py +0 -0
  46. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  47. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/device_control.py +0 -0
  48. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/enhanced.py +0 -0
  49. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/helpers.py +0 -0
  50. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/registry.py +0 -0
  51. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/smart_search.py +0 -0
  52. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_areas.py +0 -0
  53. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  54. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  55. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_calendar.py +0 -0
  56. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_camera.py +0 -0
  57. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_categories.py +0 -0
  58. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  59. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  60. {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
  61. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  62. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  63. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_entities.py +0 -0
  64. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  65. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_groups.py +0 -0
  66. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_hacs.py +0 -0
  67. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_history.py +0 -0
  68. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_integrations.py +0 -0
  69. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_labels.py +0 -0
  70. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  71. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_registry.py +0 -0
  72. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_resources.py +0 -0
  73. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_search.py +0 -0
  74. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_service.py +0 -0
  75. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_services.py +0 -0
  76. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_system.py +0 -0
  77. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_todo.py +0 -0
  78. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_traces.py +0 -0
  79. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_updates.py +0 -0
  80. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_utility.py +0 -0
  81. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  82. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  83. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/tools_zones.py +0 -0
  84. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/tools/util_helpers.py +0 -0
  85. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/transforms/__init__.py +0 -0
  86. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/transforms/categorized_search.py +0 -0
  87. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/__init__.py +0 -0
  88. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/domain_handlers.py +0 -0
  89. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  90. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/operation_manager.py +0 -0
  91. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/python_sandbox.py +0 -0
  92. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp/utils/usage_logger.py +0 -0
  93. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  94. {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
  95. {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
  96. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  97. {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
  98. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/tests/__init__.py +0 -0
  99. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/tests/test_constants.py +0 -0
  100. {ha_mcp_dev-7.2.0.dev327 → ha_mcp_dev-7.2.0.dev329}/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.2.0.dev327
3
+ Version: 7.2.0.dev329
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.2.0.dev327"
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(create_error_response(
91
- ErrorCode.RESOURCE_NOT_FOUND,
92
- "Supervisor API not available",
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
- suggestions=[
95
- "This feature requires Home Assistant OS or Supervised installation",
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(create_validation_error(
314
- "Path contains '..' traversal component",
315
- parameter="path",
316
- details=f"Rejected path: {path}",
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(create_error_response(
330
- ErrorCode.VALIDATION_FAILED,
331
- f"Add-on '{addon_name}' does not support Ingress",
332
- suggestions=[
333
- "Use the 'port' parameter for WebSocket connections to this add-on",
334
- f"Use ha_get_addon(slug='{slug}') to see available ports",
335
- ],
336
- context={"slug": slug},
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(create_error_response(
342
- ErrorCode.SERVICE_CALL_FAILED,
343
- f"Add-on '{addon_name}' is not running (state: {addon.get('state')})",
344
- suggestions=[
345
- f"Start the add-on first with: ha_call_service('hassio', 'addon_start', {{'addon': '{slug}'}})",
346
- ],
347
- context={"slug": slug, "state": addon.get("state")},
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(create_error_response(
355
- ErrorCode.INTERNAL_ERROR,
356
- f"Add-on '{addon_name}' is missing ip_address",
357
- context={"slug": slug},
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(create_error_response(
364
- ErrorCode.INTERNAL_ERROR,
365
- f"Add-on '{addon_name}' is missing network info",
366
- context={"slug": slug},
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(create_error_response(
442
- ErrorCode.SERVICE_CALL_FAILED,
443
- f"WebSocket handshake failed with '{addon_name}': {e!s}",
444
- suggestions=[
445
- "Check that the add-on supports WebSocket on this path",
446
- f"Use ha_get_addon(slug='{slug}') to inspect available endpoints",
447
- ],
448
- context={"slug": slug, "path": path},
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(create_error_response(
452
- ErrorCode.SERVICE_CALL_FAILED,
453
- f"WebSocket connection to '{addon_name}' closed unexpectedly: {e!s}",
454
- suggestions=[
455
- "The add-on may have rejected the connection or restarted",
456
- "Try again or check add-on logs for errors",
457
- ],
458
- context={"slug": slug, "path": path},
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(create_timeout_error(
462
- f"WebSocket connection to '{addon_name}'",
463
- timeout,
464
- details=f"path={path}",
465
- context={"slug": slug, "path": path},
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(create_connection_error(
469
- f"Failed to connect to add-on '{addon_name}' WebSocket: {e!s}",
470
- details="Check that the add-on is running and the port is correct",
471
- context={"slug": slug},
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(create_validation_error(
557
- "Path contains '..' traversal component",
558
- parameter="path",
559
- details=f"Rejected path: {path}",
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(create_error_response(
573
- ErrorCode.VALIDATION_FAILED,
574
- f"Add-on '{addon_name}' does not support Ingress",
575
- suggestions=[
576
- "Check if this add-on exposes a direct port instead",
577
- f"Use ha_get_addon(slug='{slug}') to see port mappings",
578
- "Use the 'port' parameter to connect to a direct access port",
579
- ],
580
- context={"slug": slug},
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(create_error_response(
586
- ErrorCode.SERVICE_CALL_FAILED,
587
- f"Add-on '{addon_name}' is not running (state: {addon.get('state')})",
588
- suggestions=[
589
- f"Start the add-on first with: ha_call_service('hassio', 'addon_start', {{'addon': '{slug}'}})",
590
- ],
591
- context={"slug": slug, "state": addon.get("state")},
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(create_error_response(
603
- ErrorCode.INTERNAL_ERROR,
604
- f"Add-on '{addon_name}' is missing ip_address",
605
- context={"slug": slug, "ip_address": addon_ip},
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(create_error_response(
613
- ErrorCode.INTERNAL_ERROR,
614
- f"Add-on '{addon_name}' is missing network info (ip_address or ingress_port)",
615
- context={"slug": slug, "ip_address": addon_ip, "ingress_port": ingress_port},
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(create_timeout_error(
651
- f"add-on API call to '{addon_name}'",
652
- timeout,
653
- details=f"path={path}, method={method}",
654
- context={"slug": slug, "path": path},
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(create_connection_error(
658
- f"Failed to connect to add-on '{addon_name}': {e!s}",
659
- details="Check that the add-on is running and Home Assistant Ingress is working",
660
- context={"slug": slug},
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"] = f"Response truncated to {_MAX_RESPONSE_SIZE // 1024}KB. The full response was larger."
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(tags={"Add-ons"}, annotations={"idempotentHint": True, "readOnlyHint": True, "title": "Get Add-ons"})
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(create_validation_error(
871
- f"Invalid source: {source}. Must be 'installed' or 'available'.",
872
- parameter="source",
873
- details="Valid sources: installed, available",
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(create_validation_error(
997
- f"Invalid HTTP method: {method}. Must be one of: {', '.join(sorted(valid_methods))}",
998
- parameter="method",
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.2.0.dev327
3
+ Version: 7.2.0.dev329
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