ha-mcp-dev 6.5.0.dev182__tar.gz → 6.5.0.dev184__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-6.5.0.dev182/src/ha_mcp_dev.egg-info → ha_mcp_dev-6.5.0.dev184}/PKG-INFO +1 -1
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/pyproject.toml +1 -1
- ha_mcp_dev-6.5.0.dev184/src/ha_mcp/tools/helpers.py +257 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_config_automations.py +17 -11
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_registry.py +3 -3
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_service.py +16 -11
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- ha_mcp_dev-6.5.0.dev182/src/ha_mcp/tools/helpers.py +0 -184
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/LICENSE +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/MANIFEST.in +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/README.md +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/setup.cfg +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/resources/card_types.json +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/resources/dashboard_guide.md +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_config_info.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/tests/__init__.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/tests/test_constants.py +0 -0
- {ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/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 = "6.5.0.
|
|
7
|
+
version = "6.5.0.dev184"
|
|
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"
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reusable helper functions for MCP tools.
|
|
3
|
+
|
|
4
|
+
Centralized utilities that can be shared across multiple tool implementations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Literal, NoReturn, overload
|
|
12
|
+
|
|
13
|
+
from fastmcp.exceptions import ToolError
|
|
14
|
+
|
|
15
|
+
from ..client.rest_client import (
|
|
16
|
+
HomeAssistantAPIError,
|
|
17
|
+
HomeAssistantAuthError,
|
|
18
|
+
HomeAssistantConnectionError,
|
|
19
|
+
)
|
|
20
|
+
from ..client.websocket_client import HomeAssistantWebSocketClient
|
|
21
|
+
from ..errors import (
|
|
22
|
+
ErrorCode,
|
|
23
|
+
create_auth_error,
|
|
24
|
+
create_connection_error,
|
|
25
|
+
create_entity_not_found_error,
|
|
26
|
+
create_error_response,
|
|
27
|
+
create_timeout_error,
|
|
28
|
+
create_validation_error,
|
|
29
|
+
)
|
|
30
|
+
from ..utils.usage_logger import log_tool_call
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def raise_tool_error(error_response: dict[str, Any]) -> NoReturn:
|
|
36
|
+
"""
|
|
37
|
+
Raise a ToolError with structured error information.
|
|
38
|
+
|
|
39
|
+
This function converts a structured error response dictionary into a ToolError
|
|
40
|
+
exception, which signals to MCP clients that the tool execution failed via
|
|
41
|
+
the isError flag in the protocol response.
|
|
42
|
+
|
|
43
|
+
The structured error information is preserved as JSON in the error message,
|
|
44
|
+
allowing AI agents to parse and act on the detailed error information.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
error_response: Structured error response dictionary with 'success': False
|
|
48
|
+
and 'error' containing code, message, suggestions, etc.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ToolError: Always raises with the JSON-serialized error response
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> error = create_error_response(
|
|
55
|
+
... ErrorCode.ENTITY_NOT_FOUND,
|
|
56
|
+
... "Entity light.nonexistent not found"
|
|
57
|
+
... )
|
|
58
|
+
>>> raise_tool_error(error) # Raises ToolError with isError=true
|
|
59
|
+
"""
|
|
60
|
+
raise ToolError(json.dumps(error_response, indent=2, default=str))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def get_connected_ws_client(
|
|
64
|
+
base_url: str, token: str
|
|
65
|
+
) -> tuple[HomeAssistantWebSocketClient | None, dict[str, Any] | None]:
|
|
66
|
+
"""
|
|
67
|
+
Create and connect a WebSocket client.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
base_url: Home Assistant base URL
|
|
71
|
+
token: Authentication token
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Tuple of (ws_client, error_dict). If connection fails, ws_client is None.
|
|
75
|
+
"""
|
|
76
|
+
ws_client = HomeAssistantWebSocketClient(base_url, token)
|
|
77
|
+
connected = await ws_client.connect()
|
|
78
|
+
if not connected:
|
|
79
|
+
return None, create_connection_error(
|
|
80
|
+
"Failed to connect to Home Assistant WebSocket",
|
|
81
|
+
details="WebSocket connection could not be established",
|
|
82
|
+
)
|
|
83
|
+
return ws_client, None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@overload
|
|
87
|
+
def exception_to_structured_error(
|
|
88
|
+
error: Exception,
|
|
89
|
+
context: dict[str, Any] | None = None,
|
|
90
|
+
*,
|
|
91
|
+
raise_error: Literal[False] = False,
|
|
92
|
+
) -> dict[str, Any]: ...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@overload
|
|
96
|
+
def exception_to_structured_error(
|
|
97
|
+
error: Exception,
|
|
98
|
+
context: dict[str, Any] | None = None,
|
|
99
|
+
*,
|
|
100
|
+
raise_error: Literal[True],
|
|
101
|
+
) -> NoReturn: ...
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def exception_to_structured_error(
|
|
105
|
+
error: Exception,
|
|
106
|
+
context: dict[str, Any] | None = None,
|
|
107
|
+
*,
|
|
108
|
+
raise_error: bool = False,
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
"""
|
|
111
|
+
Convert an exception to a structured error response.
|
|
112
|
+
|
|
113
|
+
This function maps common exception types to appropriate error codes
|
|
114
|
+
and creates informative error responses.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
error: The exception to convert
|
|
118
|
+
context: Additional context to include in the response
|
|
119
|
+
raise_error: If True, raises ToolError with the structured error.
|
|
120
|
+
If False (default), returns the error dict.
|
|
121
|
+
|
|
122
|
+
NOTE: The default will change to True in a future PR once
|
|
123
|
+
all tools are updated to use ToolError. New code should
|
|
124
|
+
explicitly pass raise_error=True for forward compatibility.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Structured error response dictionary (only if raise_error=False)
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ToolError: If raise_error=True, raises with JSON-serialized error
|
|
131
|
+
"""
|
|
132
|
+
error_str = str(error).lower()
|
|
133
|
+
error_msg = str(error)
|
|
134
|
+
|
|
135
|
+
error_response: dict[str, Any]
|
|
136
|
+
|
|
137
|
+
# Handle specific exception types
|
|
138
|
+
if isinstance(error, HomeAssistantConnectionError):
|
|
139
|
+
if "timeout" in error_str:
|
|
140
|
+
error_response = create_connection_error(error_msg, timeout=True)
|
|
141
|
+
else:
|
|
142
|
+
error_response = create_connection_error(error_msg)
|
|
143
|
+
|
|
144
|
+
elif isinstance(error, HomeAssistantAuthError):
|
|
145
|
+
if "expired" in error_str:
|
|
146
|
+
error_response = create_auth_error(error_msg, expired=True)
|
|
147
|
+
else:
|
|
148
|
+
error_response = create_auth_error(error_msg)
|
|
149
|
+
|
|
150
|
+
elif isinstance(error, HomeAssistantAPIError):
|
|
151
|
+
# Check for specific error patterns
|
|
152
|
+
match error.status_code:
|
|
153
|
+
case 404:
|
|
154
|
+
# Entity or resource not found
|
|
155
|
+
entity_id = context.get("entity_id") if context else None
|
|
156
|
+
if entity_id:
|
|
157
|
+
error_response = create_entity_not_found_error(entity_id, details=error_msg)
|
|
158
|
+
else:
|
|
159
|
+
error_response = create_error_response(
|
|
160
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
161
|
+
error_msg,
|
|
162
|
+
context=context,
|
|
163
|
+
)
|
|
164
|
+
case 401 | 403:
|
|
165
|
+
error_response = create_auth_error(error_msg)
|
|
166
|
+
case 400:
|
|
167
|
+
error_response = create_validation_error(error_msg, context=context)
|
|
168
|
+
case _:
|
|
169
|
+
# Generic API error
|
|
170
|
+
error_response = create_error_response(
|
|
171
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
172
|
+
error_msg,
|
|
173
|
+
context=context,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
elif isinstance(error, TimeoutError):
|
|
177
|
+
operation = context.get("operation", "request") if context else "request"
|
|
178
|
+
timeout_seconds = context.get("timeout_seconds", 30) if context else 30
|
|
179
|
+
error_response = create_timeout_error(operation, timeout_seconds, details=error_msg)
|
|
180
|
+
|
|
181
|
+
elif isinstance(error, ValueError):
|
|
182
|
+
error_response = create_validation_error(error_msg)
|
|
183
|
+
|
|
184
|
+
# Check for common error patterns in error message
|
|
185
|
+
elif "not found" in error_str or "404" in error_str:
|
|
186
|
+
entity_id = context.get("entity_id") if context else None
|
|
187
|
+
if entity_id:
|
|
188
|
+
error_response = create_entity_not_found_error(entity_id, details=error_msg)
|
|
189
|
+
else:
|
|
190
|
+
error_response = create_error_response(
|
|
191
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
192
|
+
error_msg,
|
|
193
|
+
context=context,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
elif "timeout" in error_str:
|
|
197
|
+
error_response = create_timeout_error("operation", 30, details=error_msg)
|
|
198
|
+
|
|
199
|
+
elif "connection" in error_str or "connect" in error_str:
|
|
200
|
+
error_response = create_connection_error(error_msg)
|
|
201
|
+
|
|
202
|
+
elif "auth" in error_str or "token" in error_str or "401" in error_str:
|
|
203
|
+
error_response = create_auth_error(error_msg)
|
|
204
|
+
|
|
205
|
+
else:
|
|
206
|
+
# Default to internal error
|
|
207
|
+
error_response = create_error_response(
|
|
208
|
+
ErrorCode.INTERNAL_ERROR,
|
|
209
|
+
error_msg,
|
|
210
|
+
details="An unexpected error occurred",
|
|
211
|
+
context=context,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if raise_error:
|
|
215
|
+
raise_tool_error(error_response)
|
|
216
|
+
|
|
217
|
+
return error_response
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def log_tool_usage(func: Any) -> Any:
|
|
221
|
+
"""
|
|
222
|
+
Decorator to automatically log MCP tool usage.
|
|
223
|
+
|
|
224
|
+
Tracks execution time, success/failure, and response size for all tool calls.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
@functools.wraps(func)
|
|
228
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
229
|
+
start_time = time.time()
|
|
230
|
+
tool_name = func.__name__
|
|
231
|
+
success = True
|
|
232
|
+
error_message = None
|
|
233
|
+
response_size = None
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
result = await func(*args, **kwargs)
|
|
237
|
+
if isinstance(result, str):
|
|
238
|
+
response_size = len(result.encode("utf-8"))
|
|
239
|
+
elif hasattr(result, "__len__"):
|
|
240
|
+
response_size = len(str(result).encode("utf-8"))
|
|
241
|
+
return result
|
|
242
|
+
except Exception as e:
|
|
243
|
+
success = False
|
|
244
|
+
error_message = str(e)
|
|
245
|
+
raise
|
|
246
|
+
finally:
|
|
247
|
+
execution_time_ms = (time.time() - start_time) * 1000
|
|
248
|
+
log_tool_call(
|
|
249
|
+
tool_name=tool_name,
|
|
250
|
+
parameters=kwargs,
|
|
251
|
+
execution_time_ms=execution_time_ms,
|
|
252
|
+
success=success,
|
|
253
|
+
error_message=error_message,
|
|
254
|
+
response_size_bytes=response_size,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return wrapper
|
{ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -8,6 +8,7 @@ Home Assistant automation configurations.
|
|
|
8
8
|
import logging
|
|
9
9
|
from typing import Annotated, Any, cast
|
|
10
10
|
|
|
11
|
+
from fastmcp.exceptions import ToolError
|
|
11
12
|
from pydantic import Field
|
|
12
13
|
|
|
13
14
|
from ..errors import (
|
|
@@ -15,7 +16,7 @@ from ..errors import (
|
|
|
15
16
|
create_resource_not_found_error,
|
|
16
17
|
create_validation_error,
|
|
17
18
|
)
|
|
18
|
-
from .helpers import exception_to_structured_error, log_tool_usage
|
|
19
|
+
from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
|
|
19
20
|
from .util_helpers import parse_json_param
|
|
20
21
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
@@ -249,12 +250,13 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
249
250
|
)
|
|
250
251
|
error_response["action"] = "get"
|
|
251
252
|
error_response["reason"] = "not_found"
|
|
252
|
-
|
|
253
|
+
raise_tool_error(error_response)
|
|
253
254
|
|
|
254
255
|
logger.error(f"Error getting automation: {e}")
|
|
255
256
|
error_response = exception_to_structured_error(
|
|
256
257
|
e,
|
|
257
258
|
context={"identifier": identifier, "action": "get"},
|
|
259
|
+
raise_error=False,
|
|
258
260
|
)
|
|
259
261
|
# Add automation-specific suggestions
|
|
260
262
|
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
@@ -263,7 +265,7 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
263
265
|
"Check Home Assistant connection",
|
|
264
266
|
"Use ha_get_domain_docs('automation') for configuration help",
|
|
265
267
|
]
|
|
266
|
-
|
|
268
|
+
raise_tool_error(error_response)
|
|
267
269
|
|
|
268
270
|
@mcp.tool(
|
|
269
271
|
annotations={
|
|
@@ -411,19 +413,19 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
411
413
|
try:
|
|
412
414
|
parsed_config = parse_json_param(config, "config")
|
|
413
415
|
except ValueError as e:
|
|
414
|
-
|
|
416
|
+
raise_tool_error(create_validation_error(
|
|
415
417
|
f"Invalid config parameter: {e}",
|
|
416
418
|
parameter="config",
|
|
417
419
|
invalid_json=True,
|
|
418
|
-
)
|
|
420
|
+
))
|
|
419
421
|
|
|
420
422
|
# Ensure config is a dict
|
|
421
423
|
if parsed_config is None or not isinstance(parsed_config, dict):
|
|
422
|
-
|
|
424
|
+
raise_tool_error(create_validation_error(
|
|
423
425
|
"Config parameter must be a JSON object",
|
|
424
426
|
parameter="config",
|
|
425
427
|
details=f"Received type: {type(parsed_config).__name__}",
|
|
426
|
-
)
|
|
428
|
+
))
|
|
427
429
|
|
|
428
430
|
config_dict = cast(dict[str, Any], parsed_config)
|
|
429
431
|
|
|
@@ -441,11 +443,11 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
441
443
|
|
|
442
444
|
missing_fields = [f for f in required_fields if f not in config_dict]
|
|
443
445
|
if missing_fields:
|
|
444
|
-
|
|
446
|
+
raise_tool_error(create_config_error(
|
|
445
447
|
f"Missing required fields: {', '.join(missing_fields)}",
|
|
446
448
|
identifier=identifier,
|
|
447
449
|
missing_fields=missing_fields,
|
|
448
|
-
)
|
|
450
|
+
))
|
|
449
451
|
|
|
450
452
|
result = await client.upsert_automation_config(config_dict, identifier)
|
|
451
453
|
return {
|
|
@@ -454,11 +456,14 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
454
456
|
"config_provided": config_dict,
|
|
455
457
|
}
|
|
456
458
|
|
|
459
|
+
except ToolError:
|
|
460
|
+
raise
|
|
457
461
|
except Exception as e:
|
|
458
462
|
logger.error(f"Error upserting automation: {e}")
|
|
459
463
|
error_response = exception_to_structured_error(
|
|
460
464
|
e,
|
|
461
465
|
context={"identifier": identifier},
|
|
466
|
+
raise_error=False,
|
|
462
467
|
)
|
|
463
468
|
# Add automation-specific suggestions
|
|
464
469
|
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
@@ -469,7 +474,7 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
469
474
|
"Use ha_search_entities(domain_filter='automation') to find automations",
|
|
470
475
|
"Use ha_get_domain_docs('automation') for comprehensive configuration help",
|
|
471
476
|
]
|
|
472
|
-
|
|
477
|
+
raise_tool_error(error_response)
|
|
473
478
|
|
|
474
479
|
@mcp.tool(
|
|
475
480
|
annotations={
|
|
@@ -513,6 +518,7 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
513
518
|
error_response = exception_to_structured_error(
|
|
514
519
|
e,
|
|
515
520
|
context={"identifier": identifier},
|
|
521
|
+
raise_error=False,
|
|
516
522
|
)
|
|
517
523
|
error_response["action"] = "delete"
|
|
518
524
|
# Add automation-specific suggestions
|
|
@@ -522,4 +528,4 @@ def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> No
|
|
|
522
528
|
"Use entity_id format: automation.morning_routine or unique_id",
|
|
523
529
|
"Check Home Assistant connection",
|
|
524
530
|
]
|
|
525
|
-
|
|
531
|
+
raise_tool_error(error_response)
|
|
@@ -16,7 +16,7 @@ from typing import Annotated, Any
|
|
|
16
16
|
from pydantic import Field
|
|
17
17
|
|
|
18
18
|
from ..errors import ErrorCode, create_error_response
|
|
19
|
-
from .helpers import log_tool_usage
|
|
19
|
+
from .helpers import log_tool_usage, raise_tool_error
|
|
20
20
|
from .util_helpers import coerce_bool_param, parse_string_list_param
|
|
21
21
|
|
|
22
22
|
# Known voice assistant identifiers
|
|
@@ -785,10 +785,10 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
785
785
|
try:
|
|
786
786
|
parsed_labels = parse_string_list_param(labels, "labels")
|
|
787
787
|
except ValueError as e:
|
|
788
|
-
|
|
788
|
+
raise_tool_error(create_error_response(
|
|
789
789
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
790
790
|
f"Invalid labels parameter: {e}",
|
|
791
|
-
)
|
|
791
|
+
))
|
|
792
792
|
|
|
793
793
|
# Delegate to internal implementation
|
|
794
794
|
return await _update_device_internal(
|
|
@@ -7,12 +7,13 @@ This module provides service execution and WebSocket-enabled operation monitorin
|
|
|
7
7
|
from typing import Any, cast
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
|
+
from fastmcp.exceptions import ToolError
|
|
10
11
|
|
|
11
12
|
from ..errors import (
|
|
12
13
|
create_validation_error,
|
|
13
14
|
)
|
|
14
15
|
from ..client.rest_client import HomeAssistantConnectionError
|
|
15
|
-
from .helpers import exception_to_structured_error, log_tool_usage
|
|
16
|
+
from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
|
|
16
17
|
from .util_helpers import coerce_bool_param, parse_json_param
|
|
17
18
|
|
|
18
19
|
|
|
@@ -79,11 +80,11 @@ def register_service_tools(mcp, client, **kwargs):
|
|
|
79
80
|
try:
|
|
80
81
|
parsed_data = parse_json_param(data, "data")
|
|
81
82
|
except ValueError as e:
|
|
82
|
-
|
|
83
|
+
raise_tool_error(create_validation_error(
|
|
83
84
|
f"Invalid data parameter: {e}",
|
|
84
85
|
parameter="data",
|
|
85
86
|
invalid_json=True,
|
|
86
|
-
)
|
|
87
|
+
))
|
|
87
88
|
|
|
88
89
|
# Ensure service_data is a dict
|
|
89
90
|
service_data: dict[str, Any] = {}
|
|
@@ -91,11 +92,11 @@ def register_service_tools(mcp, client, **kwargs):
|
|
|
91
92
|
if isinstance(parsed_data, dict):
|
|
92
93
|
service_data = parsed_data
|
|
93
94
|
else:
|
|
94
|
-
|
|
95
|
+
raise_tool_error(create_validation_error(
|
|
95
96
|
"Data parameter must be a JSON object",
|
|
96
97
|
parameter="data",
|
|
97
98
|
details=f"Received type: {type(parsed_data).__name__}",
|
|
98
|
-
)
|
|
99
|
+
))
|
|
99
100
|
|
|
100
101
|
if entity_id:
|
|
101
102
|
service_data["entity_id"] = entity_id
|
|
@@ -154,10 +155,13 @@ def register_service_tools(mcp, client, **kwargs):
|
|
|
154
155
|
"service": service,
|
|
155
156
|
"entity_id": entity_id,
|
|
156
157
|
},
|
|
158
|
+
raise_error=False,
|
|
157
159
|
)
|
|
158
160
|
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
159
161
|
error_response["error"]["suggestions"] = _build_service_suggestions(domain, service, entity_id)
|
|
160
|
-
|
|
162
|
+
raise_tool_error(error_response)
|
|
163
|
+
except ToolError:
|
|
164
|
+
raise
|
|
161
165
|
except Exception as error:
|
|
162
166
|
# Use structured error response
|
|
163
167
|
error_response = exception_to_structured_error(
|
|
@@ -167,6 +171,7 @@ def register_service_tools(mcp, client, **kwargs):
|
|
|
167
171
|
"service": service,
|
|
168
172
|
"entity_id": entity_id,
|
|
169
173
|
},
|
|
174
|
+
raise_error=False,
|
|
170
175
|
)
|
|
171
176
|
suggestions = _build_service_suggestions(domain, service, entity_id)
|
|
172
177
|
if entity_id:
|
|
@@ -177,7 +182,7 @@ def register_service_tools(mcp, client, **kwargs):
|
|
|
177
182
|
# Merge suggestions into error response
|
|
178
183
|
if "error" in error_response and isinstance(error_response["error"], dict):
|
|
179
184
|
error_response["error"]["suggestions"] = suggestions
|
|
180
|
-
|
|
185
|
+
raise_tool_error(error_response)
|
|
181
186
|
|
|
182
187
|
@mcp.tool(annotations={"readOnlyHint": True, "title": "Get Operation Status"})
|
|
183
188
|
@log_tool_usage
|
|
@@ -204,19 +209,19 @@ def register_service_tools(mcp, client, **kwargs):
|
|
|
204
209
|
try:
|
|
205
210
|
parsed_operations = parse_json_param(operations, "operations")
|
|
206
211
|
except ValueError as e:
|
|
207
|
-
|
|
212
|
+
raise_tool_error(create_validation_error(
|
|
208
213
|
f"Invalid operations parameter: {e}",
|
|
209
214
|
parameter="operations",
|
|
210
215
|
invalid_json=True,
|
|
211
|
-
)
|
|
216
|
+
))
|
|
212
217
|
|
|
213
218
|
# Ensure operations is a list of dicts
|
|
214
219
|
if parsed_operations is None or not isinstance(parsed_operations, list):
|
|
215
|
-
|
|
220
|
+
raise_tool_error(create_validation_error(
|
|
216
221
|
"Operations parameter must be a list",
|
|
217
222
|
parameter="operations",
|
|
218
223
|
details=f"Received type: {type(parsed_operations).__name__}",
|
|
219
|
-
)
|
|
224
|
+
))
|
|
220
225
|
|
|
221
226
|
operations_list = cast(list[dict[str, Any]], parsed_operations)
|
|
222
227
|
result = await device_tools.bulk_device_control(
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Reusable helper functions for MCP tools.
|
|
3
|
-
|
|
4
|
-
Centralized utilities that can be shared across multiple tool implementations.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import functools
|
|
8
|
-
import logging
|
|
9
|
-
import time
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
from ..client.rest_client import (
|
|
13
|
-
HomeAssistantAPIError,
|
|
14
|
-
HomeAssistantAuthError,
|
|
15
|
-
HomeAssistantConnectionError,
|
|
16
|
-
)
|
|
17
|
-
from ..client.websocket_client import HomeAssistantWebSocketClient
|
|
18
|
-
from ..errors import (
|
|
19
|
-
ErrorCode,
|
|
20
|
-
create_auth_error,
|
|
21
|
-
create_connection_error,
|
|
22
|
-
create_entity_not_found_error,
|
|
23
|
-
create_error_response,
|
|
24
|
-
create_timeout_error,
|
|
25
|
-
create_validation_error,
|
|
26
|
-
)
|
|
27
|
-
from ..utils.usage_logger import log_tool_call
|
|
28
|
-
|
|
29
|
-
logger = logging.getLogger(__name__)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
async def get_connected_ws_client(
|
|
33
|
-
base_url: str, token: str
|
|
34
|
-
) -> tuple[HomeAssistantWebSocketClient | None, dict[str, Any] | None]:
|
|
35
|
-
"""
|
|
36
|
-
Create and connect a WebSocket client.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
base_url: Home Assistant base URL
|
|
40
|
-
token: Authentication token
|
|
41
|
-
|
|
42
|
-
Returns:
|
|
43
|
-
Tuple of (ws_client, error_dict). If connection fails, ws_client is None.
|
|
44
|
-
"""
|
|
45
|
-
ws_client = HomeAssistantWebSocketClient(base_url, token)
|
|
46
|
-
connected = await ws_client.connect()
|
|
47
|
-
if not connected:
|
|
48
|
-
return None, create_connection_error(
|
|
49
|
-
"Failed to connect to Home Assistant WebSocket",
|
|
50
|
-
details="WebSocket connection could not be established",
|
|
51
|
-
)
|
|
52
|
-
return ws_client, None
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def exception_to_structured_error(
|
|
56
|
-
error: Exception,
|
|
57
|
-
context: dict[str, Any] | None = None,
|
|
58
|
-
) -> dict[str, Any]:
|
|
59
|
-
"""
|
|
60
|
-
Convert an exception to a structured error response.
|
|
61
|
-
|
|
62
|
-
This function maps common exception types to appropriate error codes
|
|
63
|
-
and creates informative error responses.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
error: The exception to convert
|
|
67
|
-
context: Additional context to include in the response
|
|
68
|
-
|
|
69
|
-
Returns:
|
|
70
|
-
Structured error response dictionary
|
|
71
|
-
"""
|
|
72
|
-
error_str = str(error).lower()
|
|
73
|
-
error_msg = str(error)
|
|
74
|
-
|
|
75
|
-
# Handle specific exception types
|
|
76
|
-
if isinstance(error, HomeAssistantConnectionError):
|
|
77
|
-
if "timeout" in error_str:
|
|
78
|
-
return create_connection_error(error_msg, timeout=True)
|
|
79
|
-
return create_connection_error(error_msg)
|
|
80
|
-
|
|
81
|
-
if isinstance(error, HomeAssistantAuthError):
|
|
82
|
-
if "expired" in error_str:
|
|
83
|
-
return create_auth_error(error_msg, expired=True)
|
|
84
|
-
return create_auth_error(error_msg)
|
|
85
|
-
|
|
86
|
-
if isinstance(error, HomeAssistantAPIError):
|
|
87
|
-
# Check for specific error patterns
|
|
88
|
-
if error.status_code == 404:
|
|
89
|
-
# Entity or resource not found
|
|
90
|
-
entity_id = context.get("entity_id") if context else None
|
|
91
|
-
if entity_id:
|
|
92
|
-
return create_entity_not_found_error(entity_id, details=error_msg)
|
|
93
|
-
return create_error_response(
|
|
94
|
-
ErrorCode.RESOURCE_NOT_FOUND,
|
|
95
|
-
error_msg,
|
|
96
|
-
context=context,
|
|
97
|
-
)
|
|
98
|
-
if error.status_code == 401:
|
|
99
|
-
return create_auth_error(error_msg)
|
|
100
|
-
if error.status_code == 400:
|
|
101
|
-
return create_validation_error(error_msg, context=context)
|
|
102
|
-
|
|
103
|
-
# Generic API error
|
|
104
|
-
return create_error_response(
|
|
105
|
-
ErrorCode.SERVICE_CALL_FAILED,
|
|
106
|
-
error_msg,
|
|
107
|
-
context=context,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
if isinstance(error, TimeoutError):
|
|
111
|
-
operation = context.get("operation", "request") if context else "request"
|
|
112
|
-
timeout_seconds = context.get("timeout_seconds", 30) if context else 30
|
|
113
|
-
return create_timeout_error(operation, timeout_seconds, details=error_msg)
|
|
114
|
-
|
|
115
|
-
if isinstance(error, ValueError):
|
|
116
|
-
return create_validation_error(error_msg)
|
|
117
|
-
|
|
118
|
-
# Check for common error patterns in error message
|
|
119
|
-
if "not found" in error_str or "404" in error_str:
|
|
120
|
-
entity_id = context.get("entity_id") if context else None
|
|
121
|
-
if entity_id:
|
|
122
|
-
return create_entity_not_found_error(entity_id, details=error_msg)
|
|
123
|
-
return create_error_response(
|
|
124
|
-
ErrorCode.RESOURCE_NOT_FOUND,
|
|
125
|
-
error_msg,
|
|
126
|
-
context=context,
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
if "timeout" in error_str:
|
|
130
|
-
return create_timeout_error("operation", 30, details=error_msg)
|
|
131
|
-
|
|
132
|
-
if "connection" in error_str or "connect" in error_str:
|
|
133
|
-
return create_connection_error(error_msg)
|
|
134
|
-
|
|
135
|
-
if "auth" in error_str or "token" in error_str or "401" in error_str:
|
|
136
|
-
return create_auth_error(error_msg)
|
|
137
|
-
|
|
138
|
-
# Default to internal error
|
|
139
|
-
return create_error_response(
|
|
140
|
-
ErrorCode.INTERNAL_ERROR,
|
|
141
|
-
error_msg,
|
|
142
|
-
details="An unexpected error occurred",
|
|
143
|
-
context=context,
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def log_tool_usage(func: Any) -> Any:
|
|
148
|
-
"""
|
|
149
|
-
Decorator to automatically log MCP tool usage.
|
|
150
|
-
|
|
151
|
-
Tracks execution time, success/failure, and response size for all tool calls.
|
|
152
|
-
"""
|
|
153
|
-
|
|
154
|
-
@functools.wraps(func)
|
|
155
|
-
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
156
|
-
start_time = time.time()
|
|
157
|
-
tool_name = func.__name__
|
|
158
|
-
success = True
|
|
159
|
-
error_message = None
|
|
160
|
-
response_size = None
|
|
161
|
-
|
|
162
|
-
try:
|
|
163
|
-
result = await func(*args, **kwargs)
|
|
164
|
-
if isinstance(result, str):
|
|
165
|
-
response_size = len(result.encode("utf-8"))
|
|
166
|
-
elif hasattr(result, "__len__"):
|
|
167
|
-
response_size = len(str(result).encode("utf-8"))
|
|
168
|
-
return result
|
|
169
|
-
except Exception as e:
|
|
170
|
-
success = False
|
|
171
|
-
error_message = str(e)
|
|
172
|
-
raise
|
|
173
|
-
finally:
|
|
174
|
-
execution_time_ms = (time.time() - start_time) * 1000
|
|
175
|
-
log_tool_call(
|
|
176
|
-
tool_name=tool_name,
|
|
177
|
-
parameters=kwargs,
|
|
178
|
-
execution_time_ms=execution_time_ms,
|
|
179
|
-
success=success,
|
|
180
|
-
error_message=error_message,
|
|
181
|
-
response_size_bytes=response_size,
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
return wrapper
|
|
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
|
|
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-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp/tools/tools_voice_assistant.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
|
{ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-6.5.0.dev182 → ha_mcp_dev-6.5.0.dev184}/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
|