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.
- ha_mcp/__init__.py +51 -0
- ha_mcp/__main__.py +753 -0
- ha_mcp/_pypi_marker +0 -0
- ha_mcp/auth/__init__.py +10 -0
- ha_mcp/auth/consent_form.py +501 -0
- ha_mcp/auth/provider.py +786 -0
- ha_mcp/client/__init__.py +10 -0
- ha_mcp/client/rest_client.py +924 -0
- ha_mcp/client/websocket_client.py +656 -0
- ha_mcp/client/websocket_listener.py +340 -0
- ha_mcp/config.py +175 -0
- ha_mcp/errors.py +399 -0
- ha_mcp/py.typed +0 -0
- ha_mcp/resources/card_types.json +48 -0
- ha_mcp/resources/dashboard_guide.md +573 -0
- ha_mcp/server.py +189 -0
- ha_mcp/smoke_test.py +108 -0
- ha_mcp/tools/__init__.py +11 -0
- ha_mcp/tools/backup.py +457 -0
- ha_mcp/tools/device_control.py +687 -0
- ha_mcp/tools/enhanced.py +191 -0
- ha_mcp/tools/helpers.py +184 -0
- ha_mcp/tools/registry.py +199 -0
- ha_mcp/tools/smart_search.py +797 -0
- ha_mcp/tools/tools_addons.py +343 -0
- ha_mcp/tools/tools_areas.py +478 -0
- ha_mcp/tools/tools_blueprints.py +279 -0
- ha_mcp/tools/tools_bug_report.py +570 -0
- ha_mcp/tools/tools_calendar.py +391 -0
- ha_mcp/tools/tools_camera.py +149 -0
- ha_mcp/tools/tools_config_automations.py +508 -0
- ha_mcp/tools/tools_config_dashboards.py +1502 -0
- ha_mcp/tools/tools_config_entry_flow.py +223 -0
- ha_mcp/tools/tools_config_helpers.py +925 -0
- ha_mcp/tools/tools_config_info.py +258 -0
- ha_mcp/tools/tools_config_scripts.py +267 -0
- ha_mcp/tools/tools_entities.py +76 -0
- ha_mcp/tools/tools_filesystem.py +589 -0
- ha_mcp/tools/tools_groups.py +306 -0
- ha_mcp/tools/tools_hacs.py +787 -0
- ha_mcp/tools/tools_history.py +714 -0
- ha_mcp/tools/tools_integrations.py +297 -0
- ha_mcp/tools/tools_labels.py +713 -0
- ha_mcp/tools/tools_mcp_component.py +305 -0
- ha_mcp/tools/tools_registry.py +1115 -0
- ha_mcp/tools/tools_resources.py +622 -0
- ha_mcp/tools/tools_search.py +573 -0
- ha_mcp/tools/tools_service.py +211 -0
- ha_mcp/tools/tools_services.py +352 -0
- ha_mcp/tools/tools_system.py +363 -0
- ha_mcp/tools/tools_todo.py +530 -0
- ha_mcp/tools/tools_traces.py +496 -0
- ha_mcp/tools/tools_updates.py +476 -0
- ha_mcp/tools/tools_utility.py +519 -0
- ha_mcp/tools/tools_voice_assistant.py +345 -0
- ha_mcp/tools/tools_zones.py +387 -0
- ha_mcp/tools/util_helpers.py +199 -0
- ha_mcp/utils/__init__.py +30 -0
- ha_mcp/utils/domain_handlers.py +380 -0
- ha_mcp/utils/fuzzy_search.py +339 -0
- ha_mcp/utils/operation_manager.py +442 -0
- ha_mcp/utils/usage_logger.py +290 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/METADATA +229 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/RECORD +71 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/WHEEL +5 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/entry_points.txt +7 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/licenses/LICENSE +21 -0
- ha_mcp_dev-6.3.1.dev137.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_constants.py +15 -0
- tests/test_env_manager.py +336 -0
ha_mcp/server.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Smart MCP Server implementation.
|
|
3
|
+
|
|
4
|
+
Implements lazy initialization pattern for improved startup time:
|
|
5
|
+
- Settings and FastMCP server are created immediately (fast)
|
|
6
|
+
- Smart tools and device tools are created lazily on first access
|
|
7
|
+
- Tool modules are discovered at startup but imported on first use
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from fastmcp import FastMCP
|
|
16
|
+
from mcp.types import Icon
|
|
17
|
+
|
|
18
|
+
from .config import get_global_settings
|
|
19
|
+
from .tools.enhanced import EnhancedToolsMixin
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .client.rest_client import HomeAssistantClient
|
|
23
|
+
from .tools.registry import ToolsRegistry
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Server icon configuration using GitHub-hosted images
|
|
28
|
+
# These icons are bundled in packaging/mcpb/ and also available via GitHub raw URLs
|
|
29
|
+
SERVER_ICONS = [
|
|
30
|
+
Icon(
|
|
31
|
+
src="https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/packaging/mcpb/icon.svg",
|
|
32
|
+
mimeType="image/svg+xml",
|
|
33
|
+
),
|
|
34
|
+
Icon(
|
|
35
|
+
src="https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/master/packaging/mcpb/icon-128.png",
|
|
36
|
+
mimeType="image/png",
|
|
37
|
+
sizes=["128x128"],
|
|
38
|
+
),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
|
|
43
|
+
"""Home Assistant MCP Server with smart tools and fuzzy search.
|
|
44
|
+
|
|
45
|
+
Uses lazy initialization to improve startup time:
|
|
46
|
+
- Client, smart_tools, device_tools are created on first access
|
|
47
|
+
- Tool modules are discovered at startup but imported when first called
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
client: HomeAssistantClient | None = None,
|
|
53
|
+
server_name: str = "ha-mcp",
|
|
54
|
+
server_version: str = "0.1.0",
|
|
55
|
+
):
|
|
56
|
+
"""Initialize the smart MCP server with lazy loading support."""
|
|
57
|
+
# Load settings first (fast operation)
|
|
58
|
+
self.settings = get_global_settings()
|
|
59
|
+
|
|
60
|
+
# Store provided client or mark for lazy creation
|
|
61
|
+
self._client: HomeAssistantClient | None = client
|
|
62
|
+
self._client_provided = client is not None
|
|
63
|
+
|
|
64
|
+
# Lazy initialization placeholders
|
|
65
|
+
self._smart_tools: Any = None
|
|
66
|
+
self._device_tools: Any = None
|
|
67
|
+
self._tools_registry: ToolsRegistry | None = None
|
|
68
|
+
|
|
69
|
+
# Get server name/version from settings if no client provided
|
|
70
|
+
if not self._client_provided:
|
|
71
|
+
server_name = self.settings.mcp_server_name
|
|
72
|
+
server_version = self.settings.mcp_server_version
|
|
73
|
+
|
|
74
|
+
# Create FastMCP server with Home Assistant icons for client UI display
|
|
75
|
+
self.mcp = FastMCP(name=server_name, version=server_version, icons=SERVER_ICONS)
|
|
76
|
+
|
|
77
|
+
# Register all tools and expert prompts
|
|
78
|
+
self._initialize_server()
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def client(self) -> HomeAssistantClient:
|
|
82
|
+
"""Lazily create and return the Home Assistant client."""
|
|
83
|
+
if self._client is None:
|
|
84
|
+
from .client.rest_client import HomeAssistantClient
|
|
85
|
+
self._client = HomeAssistantClient()
|
|
86
|
+
logger.debug("Lazily created HomeAssistantClient")
|
|
87
|
+
return self._client
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def smart_tools(self) -> Any:
|
|
91
|
+
"""Lazily create and return the smart search tools."""
|
|
92
|
+
if self._smart_tools is None:
|
|
93
|
+
from .tools.smart_search import create_smart_search_tools
|
|
94
|
+
self._smart_tools = create_smart_search_tools(self.client)
|
|
95
|
+
logger.debug("Lazily created SmartSearchTools")
|
|
96
|
+
return self._smart_tools
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def device_tools(self) -> Any:
|
|
100
|
+
"""Lazily create and return the device control tools."""
|
|
101
|
+
if self._device_tools is None:
|
|
102
|
+
from .tools.device_control import create_device_control_tools
|
|
103
|
+
self._device_tools = create_device_control_tools(self.client)
|
|
104
|
+
logger.debug("Lazily created DeviceControlTools")
|
|
105
|
+
return self._device_tools
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def tools_registry(self) -> ToolsRegistry:
|
|
109
|
+
"""Lazily create and return the tools registry."""
|
|
110
|
+
if self._tools_registry is None:
|
|
111
|
+
from .tools.registry import ToolsRegistry
|
|
112
|
+
self._tools_registry = ToolsRegistry(
|
|
113
|
+
self, enabled_modules=self.settings.enabled_tool_modules
|
|
114
|
+
)
|
|
115
|
+
logger.debug("Lazily created ToolsRegistry")
|
|
116
|
+
return self._tools_registry
|
|
117
|
+
|
|
118
|
+
def _initialize_server(self) -> None:
|
|
119
|
+
"""Initialize all server components."""
|
|
120
|
+
# Register tools
|
|
121
|
+
self.tools_registry.register_all_tools()
|
|
122
|
+
|
|
123
|
+
# Register enhanced tools for first/second interaction success
|
|
124
|
+
self.register_enhanced_tools()
|
|
125
|
+
|
|
126
|
+
# Helper methods required by EnhancedToolsMixin
|
|
127
|
+
|
|
128
|
+
async def smart_entity_search(
|
|
129
|
+
self, query: str, domain_filter: str | None = None, limit: int = 10
|
|
130
|
+
) -> dict[str, Any]:
|
|
131
|
+
"""Bridge method to existing smart search implementation."""
|
|
132
|
+
return await self.smart_tools.smart_entity_search(
|
|
133
|
+
query=query, limit=limit, include_attributes=False
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
async def get_entity_state(self, entity_id: str) -> dict[str, Any]:
|
|
137
|
+
"""Bridge method to existing entity state implementation."""
|
|
138
|
+
return await self.client.get_entity_state(entity_id)
|
|
139
|
+
|
|
140
|
+
async def call_service(
|
|
141
|
+
self,
|
|
142
|
+
domain: str,
|
|
143
|
+
service: str,
|
|
144
|
+
entity_id: str | None = None,
|
|
145
|
+
data: dict | None = None,
|
|
146
|
+
) -> list[dict[str, Any]]:
|
|
147
|
+
"""Bridge method to existing service call implementation."""
|
|
148
|
+
service_data = data or {}
|
|
149
|
+
if entity_id:
|
|
150
|
+
service_data["entity_id"] = entity_id
|
|
151
|
+
return await self.client.call_service(domain, service, service_data)
|
|
152
|
+
|
|
153
|
+
async def get_entities_by_area(self, area_name: str) -> dict[str, Any]:
|
|
154
|
+
"""Bridge method to existing area functionality."""
|
|
155
|
+
return await self.smart_tools.get_entities_by_area(
|
|
156
|
+
area_query=area_name, group_by_domain=True
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async def start(self) -> None:
|
|
160
|
+
"""Start the Smart MCP server with async compatibility."""
|
|
161
|
+
logger.info(
|
|
162
|
+
f"🚀 Starting Smart {self.settings.mcp_server_name} v{self.settings.mcp_server_version}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Test connection on startup
|
|
166
|
+
try:
|
|
167
|
+
success, error = await self.client.test_connection()
|
|
168
|
+
if success:
|
|
169
|
+
config = await self.client.get_config()
|
|
170
|
+
logger.info(
|
|
171
|
+
f"✅ Successfully connected to Home Assistant: {config.get('location_name', 'Unknown')}"
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
logger.warning(f"⚠️ Failed to connect to Home Assistant: {error}")
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"❌ Error testing connection: {e}")
|
|
177
|
+
|
|
178
|
+
# Log available tools count
|
|
179
|
+
logger.info("🔧 Smart server with enhanced tools loaded")
|
|
180
|
+
|
|
181
|
+
# Run the MCP server with async compatibility
|
|
182
|
+
await self.mcp.run_async()
|
|
183
|
+
|
|
184
|
+
async def close(self) -> None:
|
|
185
|
+
"""Close the MCP server and cleanup resources."""
|
|
186
|
+
# Only close client if it was actually created
|
|
187
|
+
if self._client is not None and hasattr(self._client, "close"):
|
|
188
|
+
await self._client.close()
|
|
189
|
+
logger.info("🔧 Home Assistant Smart MCP Server closed")
|
ha_mcp/smoke_test.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Smoke test for ha-mcp binary - verifies all dependencies are bundled correctly."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# Force UTF-8 encoding on Windows for Unicode output
|
|
8
|
+
if sys.platform == "win32":
|
|
9
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
10
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
11
|
+
|
|
12
|
+
# Set dummy credentials before any imports try to use them
|
|
13
|
+
os.environ.setdefault("HOMEASSISTANT_URL", "http://smoke-test:8123")
|
|
14
|
+
os.environ.setdefault("HOMEASSISTANT_TOKEN", "smoke-test-token")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> int:
|
|
18
|
+
"""Run smoke tests and return exit code."""
|
|
19
|
+
print("=" * 60)
|
|
20
|
+
print("Home Assistant MCP Server - Smoke Test")
|
|
21
|
+
print("=" * 60)
|
|
22
|
+
|
|
23
|
+
errors = []
|
|
24
|
+
|
|
25
|
+
# Test 1: Critical library imports
|
|
26
|
+
print("\n[1/4] Testing critical library imports...")
|
|
27
|
+
critical_imports = [
|
|
28
|
+
("fastmcp", "FastMCP framework"),
|
|
29
|
+
("httpx", "HTTP client"),
|
|
30
|
+
("pydantic", "Data validation"),
|
|
31
|
+
("click", "CLI framework"),
|
|
32
|
+
("websockets", "WebSocket support"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
for module_name, description in critical_imports:
|
|
36
|
+
try:
|
|
37
|
+
__import__(module_name)
|
|
38
|
+
print(f" ✓ {module_name} ({description})")
|
|
39
|
+
except ImportError as e:
|
|
40
|
+
errors.append(f"Failed to import {module_name}: {e}")
|
|
41
|
+
print(f" ✗ {module_name} ({description}) - FAILED: {e}")
|
|
42
|
+
|
|
43
|
+
# Test 2: Server module import
|
|
44
|
+
print("\n[2/4] Testing server module import...")
|
|
45
|
+
try:
|
|
46
|
+
from ha_mcp.server import HomeAssistantSmartMCPServer
|
|
47
|
+
print(" ✓ Server module imported successfully")
|
|
48
|
+
except Exception as e:
|
|
49
|
+
errors.append(f"Failed to import server module: {e}")
|
|
50
|
+
print(f" ✗ Server module import - FAILED: {e}")
|
|
51
|
+
# Can't continue if server module fails
|
|
52
|
+
print("\n" + "=" * 60)
|
|
53
|
+
print(f"SMOKE TEST FAILED: {len(errors)} error(s)")
|
|
54
|
+
for error in errors:
|
|
55
|
+
print(f" - {error}")
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
# Test 3: Server instantiation
|
|
59
|
+
print("\n[3/4] Testing server instantiation...")
|
|
60
|
+
try:
|
|
61
|
+
server = HomeAssistantSmartMCPServer()
|
|
62
|
+
mcp = server.mcp
|
|
63
|
+
print(f" ✓ Server created: {mcp.name}")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
errors.append(f"Failed to create server: {e}")
|
|
66
|
+
print(f" ✗ Server instantiation - FAILED: {e}")
|
|
67
|
+
# Can't continue if server creation fails
|
|
68
|
+
print("\n" + "=" * 60)
|
|
69
|
+
print(f"SMOKE TEST FAILED: {len(errors)} error(s)")
|
|
70
|
+
for error in errors:
|
|
71
|
+
print(f" - {error}")
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
# Test 4: Tool discovery
|
|
75
|
+
print("\n[4/4] Testing tool discovery...")
|
|
76
|
+
try:
|
|
77
|
+
# Access the tools from the MCP instance
|
|
78
|
+
tool_count = len(mcp._tool_manager._tools)
|
|
79
|
+
print(f" ✓ Discovered {tool_count} tools")
|
|
80
|
+
|
|
81
|
+
if tool_count < 50:
|
|
82
|
+
errors.append(f"Too few tools discovered: {tool_count} (expected 50+)")
|
|
83
|
+
print(" ✗ Tool count too low (expected 50+)")
|
|
84
|
+
else:
|
|
85
|
+
# List a few tool names as examples
|
|
86
|
+
tool_names = list(mcp._tool_manager._tools.keys())[:5]
|
|
87
|
+
print(f" ✓ Sample tools: {', '.join(tool_names)}...")
|
|
88
|
+
except Exception as e:
|
|
89
|
+
errors.append(f"Failed to discover tools: {e}")
|
|
90
|
+
print(f" ✗ Tool discovery - FAILED: {e}")
|
|
91
|
+
|
|
92
|
+
# Summary
|
|
93
|
+
print("\n" + "=" * 60)
|
|
94
|
+
if errors:
|
|
95
|
+
print(f"SMOKE TEST FAILED: {len(errors)} error(s)")
|
|
96
|
+
for error in errors:
|
|
97
|
+
print(f" - {error}")
|
|
98
|
+
return 1
|
|
99
|
+
else:
|
|
100
|
+
print("SMOKE TEST PASSED: All checks successful!")
|
|
101
|
+
print(f" - All {len(critical_imports)} critical libraries imported")
|
|
102
|
+
print(" - Server instantiated successfully")
|
|
103
|
+
print(f" - {tool_count} tools discovered")
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
sys.exit(main())
|
ha_mcp/tools/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Custom tools for the Home Assistant MCP server."""
|
|
2
|
+
|
|
3
|
+
from .device_control import DeviceControlTools, create_device_control_tools
|
|
4
|
+
from .smart_search import SmartSearchTools, create_smart_search_tools
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"SmartSearchTools",
|
|
8
|
+
"create_smart_search_tools",
|
|
9
|
+
"DeviceControlTools",
|
|
10
|
+
"create_device_control_tools",
|
|
11
|
+
]
|