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/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())
@@ -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
+ ]