ha-mcp-dev 6.3.1.dev137__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. ha_mcp/__init__.py +51 -0
  2. ha_mcp/__main__.py +753 -0
  3. ha_mcp/_pypi_marker +0 -0
  4. ha_mcp/auth/__init__.py +10 -0
  5. ha_mcp/auth/consent_form.py +501 -0
  6. ha_mcp/auth/provider.py +786 -0
  7. ha_mcp/client/__init__.py +10 -0
  8. ha_mcp/client/rest_client.py +924 -0
  9. ha_mcp/client/websocket_client.py +656 -0
  10. ha_mcp/client/websocket_listener.py +340 -0
  11. ha_mcp/config.py +175 -0
  12. ha_mcp/errors.py +399 -0
  13. ha_mcp/py.typed +0 -0
  14. ha_mcp/resources/card_types.json +48 -0
  15. ha_mcp/resources/dashboard_guide.md +573 -0
  16. ha_mcp/server.py +189 -0
  17. ha_mcp/smoke_test.py +108 -0
  18. ha_mcp/tools/__init__.py +11 -0
  19. ha_mcp/tools/backup.py +457 -0
  20. ha_mcp/tools/device_control.py +687 -0
  21. ha_mcp/tools/enhanced.py +191 -0
  22. ha_mcp/tools/helpers.py +184 -0
  23. ha_mcp/tools/registry.py +199 -0
  24. ha_mcp/tools/smart_search.py +797 -0
  25. ha_mcp/tools/tools_addons.py +343 -0
  26. ha_mcp/tools/tools_areas.py +478 -0
  27. ha_mcp/tools/tools_blueprints.py +279 -0
  28. ha_mcp/tools/tools_bug_report.py +570 -0
  29. ha_mcp/tools/tools_calendar.py +391 -0
  30. ha_mcp/tools/tools_camera.py +149 -0
  31. ha_mcp/tools/tools_config_automations.py +508 -0
  32. ha_mcp/tools/tools_config_dashboards.py +1502 -0
  33. ha_mcp/tools/tools_config_entry_flow.py +223 -0
  34. ha_mcp/tools/tools_config_helpers.py +925 -0
  35. ha_mcp/tools/tools_config_info.py +258 -0
  36. ha_mcp/tools/tools_config_scripts.py +267 -0
  37. ha_mcp/tools/tools_entities.py +76 -0
  38. ha_mcp/tools/tools_filesystem.py +589 -0
  39. ha_mcp/tools/tools_groups.py +306 -0
  40. ha_mcp/tools/tools_hacs.py +787 -0
  41. ha_mcp/tools/tools_history.py +714 -0
  42. ha_mcp/tools/tools_integrations.py +297 -0
  43. ha_mcp/tools/tools_labels.py +713 -0
  44. ha_mcp/tools/tools_mcp_component.py +305 -0
  45. ha_mcp/tools/tools_registry.py +1115 -0
  46. ha_mcp/tools/tools_resources.py +622 -0
  47. ha_mcp/tools/tools_search.py +573 -0
  48. ha_mcp/tools/tools_service.py +211 -0
  49. ha_mcp/tools/tools_services.py +352 -0
  50. ha_mcp/tools/tools_system.py +363 -0
  51. ha_mcp/tools/tools_todo.py +530 -0
  52. ha_mcp/tools/tools_traces.py +496 -0
  53. ha_mcp/tools/tools_updates.py +476 -0
  54. ha_mcp/tools/tools_utility.py +519 -0
  55. ha_mcp/tools/tools_voice_assistant.py +345 -0
  56. ha_mcp/tools/tools_zones.py +387 -0
  57. ha_mcp/tools/util_helpers.py +199 -0
  58. ha_mcp/utils/__init__.py +30 -0
  59. ha_mcp/utils/domain_handlers.py +380 -0
  60. ha_mcp/utils/fuzzy_search.py +339 -0
  61. ha_mcp/utils/operation_manager.py +442 -0
  62. ha_mcp/utils/usage_logger.py +290 -0
  63. ha_mcp_dev-6.3.1.dev137.dist-info/METADATA +229 -0
  64. ha_mcp_dev-6.3.1.dev137.dist-info/RECORD +71 -0
  65. ha_mcp_dev-6.3.1.dev137.dist-info/WHEEL +5 -0
  66. ha_mcp_dev-6.3.1.dev137.dist-info/entry_points.txt +7 -0
  67. ha_mcp_dev-6.3.1.dev137.dist-info/licenses/LICENSE +21 -0
  68. ha_mcp_dev-6.3.1.dev137.dist-info/top_level.txt +2 -0
  69. tests/__init__.py +1 -0
  70. tests/test_constants.py +15 -0
  71. tests/test_env_manager.py +336 -0
ha_mcp/errors.py ADDED
@@ -0,0 +1,399 @@
1
+ """
2
+ Structured error handling for Home Assistant MCP Server.
3
+
4
+ This module provides standardized error codes, error response models, and helper
5
+ functions for creating consistent, informative error responses across all MCP tools.
6
+
7
+ The structured error format enables AI agents to:
8
+ - Diagnose issues programmatically using error codes
9
+ - Provide helpful suggestions to users
10
+ - Understand the context and details of failures
11
+ """
12
+
13
+ from enum import Enum
14
+ from typing import Any
15
+
16
+
17
+ class ErrorCode(str, Enum):
18
+ """
19
+ Standard error codes for Home Assistant MCP operations.
20
+
21
+ Error codes are grouped by category:
22
+ - CONNECTION_*: Network and connectivity issues
23
+ - AUTH_*: Authentication and authorization issues
24
+ - ENTITY_*: Entity-related issues
25
+ - SERVICE_*: Service call issues
26
+ - CONFIG_*: Configuration issues
27
+ - VALIDATION_*: Input validation issues
28
+ - TIMEOUT_*: Timeout issues
29
+ - INTERNAL_*: Internal server errors
30
+ """
31
+
32
+ # Connection errors
33
+ CONNECTION_FAILED = "CONNECTION_FAILED"
34
+ CONNECTION_TIMEOUT = "CONNECTION_TIMEOUT"
35
+ WEBSOCKET_DISCONNECTED = "WEBSOCKET_DISCONNECTED"
36
+ WEBSOCKET_NOT_AUTHENTICATED = "WEBSOCKET_NOT_AUTHENTICATED"
37
+
38
+ # Authentication errors
39
+ AUTH_INVALID_TOKEN = "AUTH_INVALID_TOKEN"
40
+ AUTH_EXPIRED = "AUTH_EXPIRED"
41
+ AUTH_INSUFFICIENT_PERMISSIONS = "AUTH_INSUFFICIENT_PERMISSIONS"
42
+
43
+ # Entity errors
44
+ ENTITY_NOT_FOUND = "ENTITY_NOT_FOUND"
45
+ ENTITY_UNAVAILABLE = "ENTITY_UNAVAILABLE"
46
+ ENTITY_INVALID_ID = "ENTITY_INVALID_ID"
47
+ ENTITY_DOMAIN_MISMATCH = "ENTITY_DOMAIN_MISMATCH"
48
+
49
+ # Service errors
50
+ SERVICE_NOT_FOUND = "SERVICE_NOT_FOUND"
51
+ SERVICE_INVALID_DOMAIN = "SERVICE_INVALID_DOMAIN"
52
+ SERVICE_INVALID_ACTION = "SERVICE_INVALID_ACTION"
53
+ SERVICE_CALL_FAILED = "SERVICE_CALL_FAILED"
54
+
55
+ # Configuration errors
56
+ CONFIG_NOT_FOUND = "CONFIG_NOT_FOUND"
57
+ CONFIG_INVALID = "CONFIG_INVALID"
58
+ CONFIG_MISSING_REQUIRED_FIELDS = "CONFIG_MISSING_REQUIRED_FIELDS"
59
+ CONFIG_VALIDATION_FAILED = "CONFIG_VALIDATION_FAILED"
60
+
61
+ # Validation errors
62
+ VALIDATION_FAILED = "VALIDATION_FAILED"
63
+ VALIDATION_INVALID_JSON = "VALIDATION_INVALID_JSON"
64
+ VALIDATION_INVALID_PARAMETER = "VALIDATION_INVALID_PARAMETER"
65
+ VALIDATION_MISSING_PARAMETER = "VALIDATION_MISSING_PARAMETER"
66
+
67
+ # Timeout errors
68
+ TIMEOUT_OPERATION = "TIMEOUT_OPERATION"
69
+ TIMEOUT_WEBSOCKET = "TIMEOUT_WEBSOCKET"
70
+ TIMEOUT_API_REQUEST = "TIMEOUT_API_REQUEST"
71
+
72
+ # Internal errors
73
+ INTERNAL_ERROR = "INTERNAL_ERROR"
74
+ INTERNAL_UNEXPECTED = "INTERNAL_UNEXPECTED"
75
+
76
+ # Resource errors
77
+ RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
78
+ RESOURCE_ALREADY_EXISTS = "RESOURCE_ALREADY_EXISTS"
79
+ RESOURCE_LOCKED = "RESOURCE_LOCKED"
80
+
81
+
82
+ # Default suggestions for common error codes
83
+ DEFAULT_SUGGESTIONS: dict[ErrorCode, list[str]] = {
84
+ ErrorCode.CONNECTION_FAILED: [
85
+ "Check if Home Assistant is running and accessible",
86
+ "Verify the HOMEASSISTANT_URL environment variable is correct",
87
+ "Check network connectivity to Home Assistant",
88
+ ],
89
+ ErrorCode.CONNECTION_TIMEOUT: [
90
+ "Home Assistant may be overloaded or slow to respond",
91
+ "Check network latency to Home Assistant",
92
+ "Try increasing the timeout value if available",
93
+ ],
94
+ ErrorCode.WEBSOCKET_DISCONNECTED: [
95
+ "WebSocket connection was lost",
96
+ "Check Home Assistant logs for errors",
97
+ "The operation may need to be retried",
98
+ ],
99
+ ErrorCode.WEBSOCKET_NOT_AUTHENTICATED: [
100
+ "WebSocket connection is not authenticated",
101
+ "Verify the access token is valid",
102
+ "Try reconnecting to Home Assistant",
103
+ ],
104
+ ErrorCode.AUTH_INVALID_TOKEN: [
105
+ "Verify the HOMEASSISTANT_TOKEN is correct",
106
+ "Generate a new long-lived access token in Home Assistant",
107
+ "Check token has not been revoked",
108
+ ],
109
+ ErrorCode.AUTH_EXPIRED: [
110
+ "The access token has expired",
111
+ "Generate a new long-lived access token in Home Assistant",
112
+ ],
113
+ ErrorCode.AUTH_INSUFFICIENT_PERMISSIONS: [
114
+ "The access token does not have sufficient permissions",
115
+ "Check token permissions in Home Assistant",
116
+ "Create a new token with required permissions",
117
+ ],
118
+ ErrorCode.ENTITY_NOT_FOUND: [
119
+ "Use ha_search_entities() to find correct entity ID",
120
+ "Verify the entity exists in Home Assistant",
121
+ "Check for typos in the entity ID",
122
+ ],
123
+ ErrorCode.ENTITY_UNAVAILABLE: [
124
+ "The entity exists but is currently unavailable",
125
+ "Check if the device is powered on and connected",
126
+ "Check Home Assistant integration status",
127
+ ],
128
+ ErrorCode.ENTITY_INVALID_ID: [
129
+ "Entity ID must be in format: domain.name",
130
+ "Use ha_search_entities() to find valid entity IDs",
131
+ ],
132
+ ErrorCode.ENTITY_DOMAIN_MISMATCH: [
133
+ "Cannot change entity to a different domain",
134
+ "Entity domain must match the original domain",
135
+ ],
136
+ ErrorCode.SERVICE_NOT_FOUND: [
137
+ "Use ha_get_domain_docs() to see available services",
138
+ "Check the service name spelling",
139
+ "Verify the domain supports this service",
140
+ ],
141
+ ErrorCode.SERVICE_INVALID_DOMAIN: [
142
+ "Use ha_get_overview() to see available domains",
143
+ "Check the domain name spelling",
144
+ ],
145
+ ErrorCode.SERVICE_INVALID_ACTION: [
146
+ "Check available actions for this domain",
147
+ "Common actions: turn_on, turn_off, toggle",
148
+ "Use ha_get_domain_docs() for service documentation",
149
+ ],
150
+ ErrorCode.SERVICE_CALL_FAILED: [
151
+ "Check the service parameters are correct",
152
+ "Verify the target entity supports this service",
153
+ "Check Home Assistant logs for detailed error",
154
+ ],
155
+ ErrorCode.CONFIG_NOT_FOUND: [
156
+ "Verify the resource exists",
157
+ "Use search tools to find the correct identifier",
158
+ ],
159
+ ErrorCode.CONFIG_INVALID: [
160
+ "Review the configuration format",
161
+ "Use ha_get_domain_docs() for configuration help",
162
+ ],
163
+ ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS: [
164
+ "Check documentation for required fields",
165
+ "Ensure all required parameters are provided",
166
+ ],
167
+ ErrorCode.VALIDATION_INVALID_JSON: [
168
+ "Ensure the parameter is valid JSON",
169
+ "Check for syntax errors in JSON",
170
+ "Use a JSON validator to verify the format",
171
+ ],
172
+ ErrorCode.VALIDATION_INVALID_PARAMETER: [
173
+ "Check the parameter type and format",
174
+ "Review the tool documentation for expected values",
175
+ ],
176
+ ErrorCode.TIMEOUT_OPERATION: [
177
+ "The operation took too long to complete",
178
+ "Home Assistant may be under heavy load",
179
+ "Try the operation again",
180
+ ],
181
+ ErrorCode.INTERNAL_ERROR: [
182
+ "An internal error occurred",
183
+ "Check Home Assistant MCP server logs",
184
+ "Report this issue if it persists",
185
+ ],
186
+ }
187
+
188
+
189
+ def create_error_response(
190
+ code: ErrorCode,
191
+ message: str,
192
+ details: str | None = None,
193
+ suggestions: list[str] | None = None,
194
+ context: dict[str, Any] | None = None,
195
+ ) -> dict[str, Any]:
196
+ """
197
+ Create a structured error response.
198
+
199
+ Args:
200
+ code: Error code from ErrorCode enum
201
+ message: Human-readable error message
202
+ details: Additional details about the error (optional)
203
+ suggestions: List of suggestions to resolve the error (optional)
204
+ context: Additional context data (e.g., entity_id, domain) (optional)
205
+
206
+ Returns:
207
+ Structured error response dictionary with success=False
208
+
209
+ Example:
210
+ >>> create_error_response(
211
+ ... ErrorCode.ENTITY_NOT_FOUND,
212
+ ... "Entity light.nonexistent not found",
213
+ ... details="No entity with this ID exists in Home Assistant",
214
+ ... context={"entity_id": "light.nonexistent"}
215
+ ... )
216
+ {
217
+ "success": False,
218
+ "error": {
219
+ "code": "ENTITY_NOT_FOUND",
220
+ "message": "Entity light.nonexistent not found",
221
+ "details": "No entity with this ID exists in Home Assistant",
222
+ "suggestion": "Use ha_search_entities() to find correct entity ID"
223
+ },
224
+ "entity_id": "light.nonexistent"
225
+ }
226
+ """
227
+ # Use provided suggestions or fall back to defaults
228
+ error_suggestions = suggestions if suggestions else DEFAULT_SUGGESTIONS.get(code, [])
229
+
230
+ error_dict: dict[str, Any] = {
231
+ "code": code.value,
232
+ "message": message,
233
+ }
234
+
235
+ if details:
236
+ error_dict["details"] = details
237
+
238
+ if error_suggestions:
239
+ # Include first suggestion as primary, all suggestions in list
240
+ error_dict["suggestion"] = error_suggestions[0]
241
+ if len(error_suggestions) > 1:
242
+ error_dict["suggestions"] = error_suggestions
243
+
244
+ response: dict[str, Any] = {
245
+ "success": False,
246
+ "error": error_dict,
247
+ }
248
+
249
+ # Add context fields at top level for easy access
250
+ if context:
251
+ response.update(context)
252
+
253
+ return response
254
+
255
+
256
+ def create_connection_error(
257
+ message: str,
258
+ details: str | None = None,
259
+ timeout: bool = False,
260
+ ) -> dict[str, Any]:
261
+ """Create a connection error response."""
262
+ code = ErrorCode.CONNECTION_TIMEOUT if timeout else ErrorCode.CONNECTION_FAILED
263
+ return create_error_response(code, message, details)
264
+
265
+
266
+ def create_auth_error(
267
+ message: str,
268
+ details: str | None = None,
269
+ expired: bool = False,
270
+ ) -> dict[str, Any]:
271
+ """Create an authentication error response."""
272
+ code = ErrorCode.AUTH_EXPIRED if expired else ErrorCode.AUTH_INVALID_TOKEN
273
+ return create_error_response(code, message, details)
274
+
275
+
276
+ def create_entity_not_found_error(
277
+ entity_id: str,
278
+ details: str | None = None,
279
+ ) -> dict[str, Any]:
280
+ """Create an entity not found error response."""
281
+ return create_error_response(
282
+ ErrorCode.ENTITY_NOT_FOUND,
283
+ f"Entity '{entity_id}' not found",
284
+ details=details or f"No entity with ID '{entity_id}' exists in Home Assistant",
285
+ context={"entity_id": entity_id},
286
+ )
287
+
288
+
289
+ def create_service_error(
290
+ domain: str,
291
+ service: str,
292
+ message: str,
293
+ details: str | None = None,
294
+ entity_id: str | None = None,
295
+ ) -> dict[str, Any]:
296
+ """Create a service call error response."""
297
+ context: dict[str, Any] = {"domain": domain, "service": service}
298
+ if entity_id:
299
+ context["entity_id"] = entity_id
300
+
301
+ return create_error_response(
302
+ ErrorCode.SERVICE_CALL_FAILED,
303
+ message,
304
+ details=details,
305
+ context=context,
306
+ )
307
+
308
+
309
+ def create_validation_error(
310
+ message: str,
311
+ parameter: str | None = None,
312
+ details: str | None = None,
313
+ invalid_json: bool = False,
314
+ context: dict[str, Any] | None = None,
315
+ ) -> dict[str, Any]:
316
+ """Create a validation error response."""
317
+ code = ErrorCode.VALIDATION_INVALID_JSON if invalid_json else ErrorCode.VALIDATION_FAILED
318
+ # Build context, prioritizing explicit context but adding parameter if provided
319
+ final_context: dict[str, Any] = {}
320
+ if context:
321
+ final_context.update(context)
322
+ if parameter:
323
+ final_context["parameter"] = parameter
324
+ return create_error_response(code, message, details, context=final_context if final_context else None)
325
+
326
+
327
+ def create_config_error(
328
+ message: str,
329
+ identifier: str | None = None,
330
+ missing_fields: list[str] | None = None,
331
+ details: str | None = None,
332
+ ) -> dict[str, Any]:
333
+ """Create a configuration error response."""
334
+ if missing_fields:
335
+ code = ErrorCode.CONFIG_MISSING_REQUIRED_FIELDS
336
+ details = details or f"Missing required fields: {', '.join(missing_fields)}"
337
+ else:
338
+ code = ErrorCode.CONFIG_INVALID
339
+
340
+ context: dict[str, Any] = {}
341
+ if identifier:
342
+ context["identifier"] = identifier
343
+ if missing_fields:
344
+ context["missing_fields"] = missing_fields
345
+
346
+ return create_error_response(code, message, details, context=context or None)
347
+
348
+
349
+ def create_timeout_error(
350
+ operation: str,
351
+ timeout_seconds: float,
352
+ details: str | None = None,
353
+ ) -> dict[str, Any]:
354
+ """Create a timeout error response."""
355
+ return create_error_response(
356
+ ErrorCode.TIMEOUT_OPERATION,
357
+ f"Operation '{operation}' timed out after {timeout_seconds}s",
358
+ details=details,
359
+ context={"operation": operation, "timeout_seconds": timeout_seconds},
360
+ )
361
+
362
+
363
+ def create_resource_not_found_error(
364
+ resource_type: str,
365
+ identifier: str,
366
+ details: str | None = None,
367
+ ) -> dict[str, Any]:
368
+ """Create a resource not found error response."""
369
+ return create_error_response(
370
+ ErrorCode.RESOURCE_NOT_FOUND,
371
+ f"{resource_type} '{identifier}' not found",
372
+ details=details,
373
+ context={"resource_type": resource_type, "identifier": identifier},
374
+ )
375
+
376
+
377
+ def is_error_response(response: dict[str, Any]) -> bool:
378
+ """Check if a response is an error response."""
379
+ return response.get("success") is False and "error" in response
380
+
381
+
382
+ def get_error_code(response: dict[str, Any]) -> str | None:
383
+ """Extract the error code from an error response."""
384
+ if is_error_response(response):
385
+ error = response.get("error", {})
386
+ if isinstance(error, dict):
387
+ return error.get("code")
388
+ return None
389
+
390
+
391
+ def get_error_message(response: dict[str, Any]) -> str | None:
392
+ """Extract the error message from an error response."""
393
+ if is_error_response(response):
394
+ error = response.get("error", {})
395
+ if isinstance(error, dict):
396
+ return error.get("message")
397
+ if isinstance(error, str):
398
+ return error
399
+ return None
ha_mcp/py.typed ADDED
File without changes
@@ -0,0 +1,48 @@
1
+ {
2
+ "card_types": [
3
+ "alarm-panel",
4
+ "area",
5
+ "button",
6
+ "calendar",
7
+ "clock",
8
+ "conditional",
9
+ "energy",
10
+ "entities",
11
+ "entity-filter",
12
+ "entity",
13
+ "gauge",
14
+ "glance",
15
+ "grid",
16
+ "heading",
17
+ "history-graph",
18
+ "horizontal-stack",
19
+ "humidifier",
20
+ "iframe",
21
+ "light",
22
+ "logbook",
23
+ "map",
24
+ "markdown",
25
+ "masonry",
26
+ "media-control",
27
+ "panel",
28
+ "picture-elements",
29
+ "picture-entity",
30
+ "picture-glance",
31
+ "picture",
32
+ "plant-status",
33
+ "sections",
34
+ "sensor",
35
+ "shopping-list",
36
+ "sidebar",
37
+ "statistic",
38
+ "statistics-graph",
39
+ "thermostat",
40
+ "tile",
41
+ "todo-list",
42
+ "vertical-stack",
43
+ "weather-forecast"
44
+ ],
45
+ "total_count": 41,
46
+ "documentation_base_url": "https://raw.githubusercontent.com/home-assistant/home-assistant.io/refs/heads/current/source/_dashboards",
47
+ "usage": "Use ha-dashboard://card-docs/{card-type} to get documentation for specific card type"
48
+ }