ha-mcp-dev 6.7.2.dev255__tar.gz → 6.7.2.dev256__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.7.2.dev255/src/ha_mcp_dev.egg-info → ha_mcp_dev-6.7.2.dev256}/PKG-INFO +1 -1
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/pyproject.toml +4 -2
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/__main__.py +25 -16
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/auth/provider.py +3 -3
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/client/rest_client.py +3 -1
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/client/websocket_client.py +1 -1
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/server.py +7 -7
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/backup.py +4 -2
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_addons.py +1 -1
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_areas.py +2 -2
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_automations.py +1 -1
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_dashboards.py +12 -13
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_mcp_component.py +2 -2
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_service.py +1 -1
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_traces.py +2 -2
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/LICENSE +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/MANIFEST.in +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/README.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/setup.cfg +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/card_types.json +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/dashboard_guide.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_info.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/tests/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/tests/test_constants.py +0 -0
- {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/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.7.2.
|
|
7
|
+
version = "6.7.2.dev256"
|
|
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"
|
|
@@ -56,7 +56,8 @@ packages = { find = { where = ["src", "."], include = ["ha_mcp*", "tests"] } }
|
|
|
56
56
|
ha_mcp = ["py.typed", "_pypi_marker", "resources/*.md", "resources/*.json", "resources/skills-vendor/**/*"]
|
|
57
57
|
|
|
58
58
|
[tool.mypy]
|
|
59
|
-
python_version = "3.
|
|
59
|
+
python_version = "3.13"
|
|
60
|
+
mypy_path = "src"
|
|
60
61
|
warn_return_any = true
|
|
61
62
|
warn_unused_configs = true
|
|
62
63
|
disallow_untyped_defs = true
|
|
@@ -72,6 +73,7 @@ explicit_package_bases = true
|
|
|
72
73
|
[[tool.mypy.overrides]]
|
|
73
74
|
module = [
|
|
74
75
|
"fastmcp.*",
|
|
76
|
+
"jq",
|
|
75
77
|
]
|
|
76
78
|
ignore_missing_imports = true
|
|
77
79
|
|
|
@@ -13,7 +13,16 @@ import signal # noqa: E402
|
|
|
13
13
|
import stat # noqa: E402
|
|
14
14
|
import sys # noqa: E402
|
|
15
15
|
import threading # noqa: E402
|
|
16
|
-
from
|
|
16
|
+
from collections.abc import Coroutine # noqa: E402
|
|
17
|
+
from typing import TYPE_CHECKING, Any # noqa: E402
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from fastmcp import FastMCP
|
|
21
|
+
|
|
22
|
+
from ha_mcp.auth.provider import HomeAssistantOAuthProvider
|
|
23
|
+
from ha_mcp.client.rest_client import HomeAssistantClient
|
|
24
|
+
from ha_mcp.config import Settings
|
|
25
|
+
from ha_mcp.server import HomeAssistantSmartMCPServer
|
|
17
26
|
|
|
18
27
|
logger = logging.getLogger(__name__)
|
|
19
28
|
|
|
@@ -25,12 +34,12 @@ class OAuthProxyClient:
|
|
|
25
34
|
The proxy allows us to inject different credentials per-request based on OAuth token claims.
|
|
26
35
|
"""
|
|
27
36
|
|
|
28
|
-
def __init__(self, auth_provider):
|
|
37
|
+
def __init__(self, auth_provider: "HomeAssistantOAuthProvider") -> None:
|
|
29
38
|
self._auth_provider = auth_provider
|
|
30
|
-
self._oauth_clients = {}
|
|
39
|
+
self._oauth_clients: dict[str, HomeAssistantClient] = {}
|
|
31
40
|
self._lock = threading.Lock()
|
|
32
41
|
|
|
33
|
-
def _get_oauth_client(self):
|
|
42
|
+
def _get_oauth_client(self) -> "HomeAssistantClient":
|
|
34
43
|
"""Get the OAuth client for the current request context."""
|
|
35
44
|
from fastmcp.server.dependencies import get_access_token
|
|
36
45
|
|
|
@@ -74,7 +83,7 @@ class OAuthProxyClient:
|
|
|
74
83
|
for client in clients:
|
|
75
84
|
await client.close()
|
|
76
85
|
|
|
77
|
-
def __getattr__(self, name):
|
|
86
|
+
def __getattr__(self, name: str) -> Any:
|
|
78
87
|
"""Forward all attribute access to the OAuth client."""
|
|
79
88
|
client = self._get_oauth_client()
|
|
80
89
|
return getattr(client, name)
|
|
@@ -217,7 +226,7 @@ For setup instructions, see:
|
|
|
217
226
|
sys.exit(1)
|
|
218
227
|
|
|
219
228
|
|
|
220
|
-
def _validate_standard_credentials(settings) -> None:
|
|
229
|
+
def _validate_standard_credentials(settings: "Settings") -> None:
|
|
221
230
|
"""Exit with error if HA credentials are OAuth sentinels in standard (non-OAuth) mode."""
|
|
222
231
|
from ha_mcp.config import OAUTH_MODE_TOKEN, OAUTH_MODE_URL
|
|
223
232
|
|
|
@@ -264,14 +273,12 @@ def _http_run_kwargs(transport: str, port: int, path: str) -> dict:
|
|
|
264
273
|
}
|
|
265
274
|
|
|
266
275
|
|
|
267
|
-
def _create_server():
|
|
276
|
+
def _create_server() -> "HomeAssistantSmartMCPServer":
|
|
268
277
|
"""Create server instance (deferred to avoid import during smoke test)."""
|
|
269
278
|
from pydantic import ValidationError
|
|
270
279
|
|
|
271
280
|
try:
|
|
272
|
-
from ha_mcp.server import
|
|
273
|
-
HomeAssistantSmartMCPServer, # type: ignore[import-not-found]
|
|
274
|
-
)
|
|
281
|
+
from ha_mcp.server import HomeAssistantSmartMCPServer
|
|
275
282
|
|
|
276
283
|
return HomeAssistantSmartMCPServer()
|
|
277
284
|
except ValidationError as e:
|
|
@@ -280,10 +287,10 @@ def _create_server():
|
|
|
280
287
|
|
|
281
288
|
|
|
282
289
|
# Lazy server creation - only create when needed
|
|
283
|
-
_server = None
|
|
290
|
+
_server: "HomeAssistantSmartMCPServer | None" = None
|
|
284
291
|
|
|
285
292
|
|
|
286
|
-
def _get_mcp():
|
|
293
|
+
def _get_mcp() -> "FastMCP":
|
|
287
294
|
"""Get the MCP instance, creating server if needed."""
|
|
288
295
|
global _server
|
|
289
296
|
if _server is None:
|
|
@@ -291,7 +298,7 @@ def _get_mcp():
|
|
|
291
298
|
return _server.mcp
|
|
292
299
|
|
|
293
300
|
|
|
294
|
-
def _get_server():
|
|
301
|
+
def _get_server() -> "HomeAssistantSmartMCPServer":
|
|
295
302
|
"""Get the server instance, creating if needed."""
|
|
296
303
|
global _server
|
|
297
304
|
if _server is None:
|
|
@@ -390,7 +397,7 @@ async def _cancel_tasks(*tasks: asyncio.Task) -> None:
|
|
|
390
397
|
pass
|
|
391
398
|
|
|
392
399
|
|
|
393
|
-
async def _run_with_shutdown(server_coro) -> None:
|
|
400
|
+
async def _run_with_shutdown(server_coro: Coroutine[Any, Any, Any]) -> None:
|
|
394
401
|
"""Run a server coroutine with graceful shutdown support.
|
|
395
402
|
|
|
396
403
|
Handles signal-based shutdown, resource cleanup, and task cancellation.
|
|
@@ -431,7 +438,7 @@ async def _run_with_shutdown(server_coro) -> None:
|
|
|
431
438
|
await _cancel_tasks(server_task, shutdown_task)
|
|
432
439
|
|
|
433
440
|
|
|
434
|
-
def _run_entrypoint(coro, label: str) -> None:
|
|
441
|
+
def _run_entrypoint(coro: Coroutine[Any, Any, Any], label: str) -> None:
|
|
435
442
|
"""Run an async entrypoint with standard exception handling."""
|
|
436
443
|
_setup_signal_handlers()
|
|
437
444
|
|
|
@@ -659,7 +666,9 @@ async def _run_oauth_server(base_url: str, port: int, path: str) -> None:
|
|
|
659
666
|
proxy_client = OAuthProxyClient(auth_provider)
|
|
660
667
|
|
|
661
668
|
global _server
|
|
662
|
-
_server = HomeAssistantSmartMCPServer(
|
|
669
|
+
_server = HomeAssistantSmartMCPServer(
|
|
670
|
+
client=proxy_client, # type: ignore[arg-type] # OAuthProxyClient forwards all HomeAssistantClient attrs via __getattr__
|
|
671
|
+
)
|
|
663
672
|
mcp = _server.mcp
|
|
664
673
|
mcp.auth = auth_provider
|
|
665
674
|
|
|
@@ -338,7 +338,7 @@ class HomeAssistantOAuthProvider(OAuthProvider):
|
|
|
338
338
|
"""
|
|
339
339
|
if client.client_id is None:
|
|
340
340
|
raise AuthorizeError(
|
|
341
|
-
error="
|
|
341
|
+
error="invalid_request",
|
|
342
342
|
error_description="Client ID is required",
|
|
343
343
|
)
|
|
344
344
|
|
|
@@ -507,7 +507,7 @@ class HomeAssistantOAuthProvider(OAuthProvider):
|
|
|
507
507
|
),
|
|
508
508
|
scopes=scopes_list,
|
|
509
509
|
expires_at=expires_at,
|
|
510
|
-
code_challenge=pending.get("code_challenge"),
|
|
510
|
+
code_challenge=pending.get("code_challenge"), # type: ignore[arg-type] # None is valid per PKCE spec (RFC 7636 §4.3); empty string would break validation
|
|
511
511
|
)
|
|
512
512
|
self.auth_codes[auth_code_value] = auth_code
|
|
513
513
|
|
|
@@ -617,7 +617,7 @@ class HomeAssistantOAuthProvider(OAuthProvider):
|
|
|
617
617
|
ha_credentials = self.ha_credentials.get(client.client_id)
|
|
618
618
|
if not ha_credentials:
|
|
619
619
|
raise TokenError(
|
|
620
|
-
"
|
|
620
|
+
"invalid_client",
|
|
621
621
|
f"No Home Assistant credentials found for client {client.client_id}",
|
|
622
622
|
)
|
|
623
623
|
|
|
@@ -618,7 +618,7 @@ class HomeAssistantClient:
|
|
|
618
618
|
Raises:
|
|
619
619
|
HomeAssistantAPIError: If flow start fails
|
|
620
620
|
"""
|
|
621
|
-
payload = {"handler": handler}
|
|
621
|
+
payload: dict[str, Any] = {"handler": handler}
|
|
622
622
|
if context:
|
|
623
623
|
payload["context"] = context
|
|
624
624
|
|
|
@@ -740,6 +740,8 @@ class HomeAssistantClient:
|
|
|
740
740
|
logger.error(f"WebSocket message failed: {e}")
|
|
741
741
|
return {"success": False, "error": str(e)}
|
|
742
742
|
|
|
743
|
+
return {"success": False, "error": "WebSocket request failed"}
|
|
744
|
+
|
|
743
745
|
async def _handle_render_template(
|
|
744
746
|
self, ws_client: Any, message: dict[str, Any]
|
|
745
747
|
) -> dict[str, Any]:
|
|
@@ -688,7 +688,7 @@ class WebSocketManager:
|
|
|
688
688
|
|
|
689
689
|
# Evict least-recently-used connection if over limit
|
|
690
690
|
if len(self._clients) > MAX_POOL_SIZE:
|
|
691
|
-
oldest_key = min(self._last_used, key=self._last_used
|
|
691
|
+
oldest_key = min(self._last_used, key=lambda k: self._last_used[k])
|
|
692
692
|
stale = self._clients.pop(oldest_key, None)
|
|
693
693
|
self._last_used.pop(oldest_key, None)
|
|
694
694
|
if stale:
|
|
@@ -11,9 +11,9 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import TYPE_CHECKING, Any
|
|
14
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
15
15
|
|
|
16
|
-
import yaml
|
|
16
|
+
import yaml # type: ignore[import-untyped]
|
|
17
17
|
from fastmcp import FastMCP
|
|
18
18
|
from mcp.types import Icon
|
|
19
19
|
|
|
@@ -314,9 +314,9 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
|
|
|
314
314
|
self, query: str, domain_filter: str | None = None, limit: int = 10
|
|
315
315
|
) -> dict[str, Any]:
|
|
316
316
|
"""Bridge method to existing smart search implementation."""
|
|
317
|
-
return await self.smart_tools.smart_entity_search(
|
|
317
|
+
return cast(dict[str, Any], await self.smart_tools.smart_entity_search(
|
|
318
318
|
query=query, limit=limit, include_attributes=False
|
|
319
|
-
)
|
|
319
|
+
))
|
|
320
320
|
|
|
321
321
|
async def get_entity_state(self, entity_id: str) -> dict[str, Any]:
|
|
322
322
|
"""Bridge method to existing entity state implementation."""
|
|
@@ -328,7 +328,7 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
|
|
|
328
328
|
service: str,
|
|
329
329
|
entity_id: str | None = None,
|
|
330
330
|
data: dict | None = None,
|
|
331
|
-
) -> list[dict[str, Any]]:
|
|
331
|
+
) -> list[dict[str, Any]] | dict[str, Any]:
|
|
332
332
|
"""Bridge method to existing service call implementation."""
|
|
333
333
|
service_data = data or {}
|
|
334
334
|
if entity_id:
|
|
@@ -337,9 +337,9 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
|
|
|
337
337
|
|
|
338
338
|
async def get_entities_by_area(self, area_name: str) -> dict[str, Any]:
|
|
339
339
|
"""Bridge method to existing area functionality."""
|
|
340
|
-
return await self.smart_tools.get_entities_by_area(
|
|
340
|
+
return cast(dict[str, Any], await self.smart_tools.get_entities_by_area(
|
|
341
341
|
area_query=area_name, group_by_domain=True
|
|
342
|
-
)
|
|
342
|
+
))
|
|
343
343
|
|
|
344
344
|
async def start(self) -> None:
|
|
345
345
|
"""Start the Smart MCP server with async compatibility."""
|
|
@@ -7,7 +7,7 @@ Provides backup creation and restoration capabilities with safety mechanisms.
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import logging
|
|
9
9
|
from datetime import datetime
|
|
10
|
-
from typing import TYPE_CHECKING, Annotated, Any
|
|
10
|
+
from typing import TYPE_CHECKING, Annotated, Any, cast
|
|
11
11
|
|
|
12
12
|
from fastmcp.exceptions import ToolError
|
|
13
13
|
from pydantic import Field
|
|
@@ -105,6 +105,7 @@ async def create_backup(
|
|
|
105
105
|
ErrorCode.CONNECTION_FAILED,
|
|
106
106
|
"Failed to connect to Home Assistant WebSocket for backup",
|
|
107
107
|
))
|
|
108
|
+
ws_client = cast(HomeAssistantWebSocketClient, ws_client)
|
|
108
109
|
|
|
109
110
|
# Get backup password
|
|
110
111
|
password, error = await _get_backup_password(ws_client)
|
|
@@ -256,6 +257,7 @@ async def restore_backup(
|
|
|
256
257
|
ErrorCode.CONNECTION_FAILED,
|
|
257
258
|
"Failed to connect to Home Assistant WebSocket for restore",
|
|
258
259
|
))
|
|
260
|
+
ws_client = cast(HomeAssistantWebSocketClient, ws_client)
|
|
259
261
|
|
|
260
262
|
# Verify backup exists
|
|
261
263
|
backup_info = await ws_client.send_command("backup/info")
|
|
@@ -354,7 +356,7 @@ async def restore_backup(
|
|
|
354
356
|
pass # Ignore errors during cleanup
|
|
355
357
|
|
|
356
358
|
|
|
357
|
-
def register_backup_tools(mcp: "FastMCP", client: HomeAssistantClient, **kwargs) -> None:
|
|
359
|
+
def register_backup_tools(mcp: "FastMCP", client: HomeAssistantClient, **kwargs: Any) -> None:
|
|
358
360
|
"""
|
|
359
361
|
Register backup and restore tools with the MCP server.
|
|
360
362
|
|
|
@@ -257,7 +257,7 @@ async def list_available_addons(
|
|
|
257
257
|
pass
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs) -> None:
|
|
260
|
+
def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -> None:
|
|
261
261
|
"""
|
|
262
262
|
Register add-on management tools with the MCP server.
|
|
263
263
|
|
|
@@ -156,7 +156,7 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
156
156
|
suggestions=["Provide a name for the new area"],
|
|
157
157
|
))
|
|
158
158
|
|
|
159
|
-
message
|
|
159
|
+
message = {
|
|
160
160
|
"type": "config/area_registry/create",
|
|
161
161
|
"name": name,
|
|
162
162
|
}
|
|
@@ -380,7 +380,7 @@ def register_area_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
380
380
|
suggestions=["Provide a name for the new floor"],
|
|
381
381
|
))
|
|
382
382
|
|
|
383
|
-
message
|
|
383
|
+
message = {
|
|
384
384
|
"type": "config/floor_registry/create",
|
|
385
385
|
"name": name,
|
|
386
386
|
}
|
{ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -169,7 +169,7 @@ def _normalize_config_for_roundtrip(config: dict[str, Any]) -> dict[str, Any]:
|
|
|
169
169
|
if "trigger" in normalized and isinstance(normalized["trigger"], list):
|
|
170
170
|
normalized["trigger"] = _normalize_trigger_keys(normalized["trigger"])
|
|
171
171
|
|
|
172
|
-
return normalized
|
|
172
|
+
return cast(dict[str, Any], normalized)
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
def _strip_empty_automation_fields(config: dict[str, Any]) -> dict[str, Any]:
|
{ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
@@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
|
|
|
30
30
|
|
|
31
31
|
# Try to import jq - it's not available on Windows ARM64
|
|
32
32
|
try:
|
|
33
|
-
import jq # noqa: F401
|
|
33
|
+
import jq # noqa: F401
|
|
34
34
|
|
|
35
35
|
JQ_AVAILABLE = True
|
|
36
36
|
except ImportError:
|
|
@@ -68,9 +68,9 @@ def _get_resources_dir() -> Path:
|
|
|
68
68
|
|
|
69
69
|
# For Python 3.9+
|
|
70
70
|
if hasattr(pkg_resources, "files"):
|
|
71
|
-
|
|
72
|
-
if hasattr(
|
|
73
|
-
return Path(str(
|
|
71
|
+
pkg_resources_dir = pkg_resources.files("ha_mcp") / "resources"
|
|
72
|
+
if hasattr(pkg_resources_dir, "__fspath__"):
|
|
73
|
+
return Path(str(pkg_resources_dir))
|
|
74
74
|
except (ImportError, AttributeError):
|
|
75
75
|
# If importlib.resources or its attributes are unavailable, fall back to relative path
|
|
76
76
|
pass
|
|
@@ -380,7 +380,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
380
380
|
# Calculate config size for progressive disclosure hint
|
|
381
381
|
config_size = len(json.dumps(config)) if isinstance(config, dict) else 0
|
|
382
382
|
|
|
383
|
-
result
|
|
383
|
+
result = {
|
|
384
384
|
"success": True,
|
|
385
385
|
"action": "get",
|
|
386
386
|
"url_path": url_path,
|
|
@@ -397,7 +397,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
397
397
|
"instead of full config replacement."
|
|
398
398
|
)
|
|
399
399
|
|
|
400
|
-
return result
|
|
400
|
+
return cast(dict[str, Any], result)
|
|
401
401
|
except ToolError:
|
|
402
402
|
raise
|
|
403
403
|
except Exception as e:
|
|
@@ -798,7 +798,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
798
798
|
))
|
|
799
799
|
|
|
800
800
|
# Fetch current dashboard config
|
|
801
|
-
get_data
|
|
801
|
+
get_data = {"type": "lovelace/config", "force": True}
|
|
802
802
|
if url_path:
|
|
803
803
|
get_data["url_path"] = url_path
|
|
804
804
|
|
|
@@ -847,7 +847,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
847
847
|
))
|
|
848
848
|
|
|
849
849
|
# Apply jq transformation
|
|
850
|
-
|
|
850
|
+
jq_result, error = _apply_jq_transform(
|
|
851
851
|
current_config, jq_transform
|
|
852
852
|
)
|
|
853
853
|
if error:
|
|
@@ -861,9 +861,10 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
861
861
|
],
|
|
862
862
|
context={"action": "jq_transform", "url_path": url_path},
|
|
863
863
|
))
|
|
864
|
+
transformed_config = cast(dict[str, Any], jq_result)
|
|
864
865
|
|
|
865
866
|
# Save transformed config
|
|
866
|
-
save_data
|
|
867
|
+
save_data = {
|
|
867
868
|
"type": "lovelace/config/save",
|
|
868
869
|
"config": transformed_config,
|
|
869
870
|
}
|
|
@@ -890,9 +891,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
890
891
|
|
|
891
892
|
# Compute new hash for potential chaining
|
|
892
893
|
# transformed_config is guaranteed to be a dict here (validated above)
|
|
893
|
-
new_config_hash = _compute_config_hash(
|
|
894
|
-
cast(dict[str, Any], transformed_config)
|
|
895
|
-
)
|
|
894
|
+
new_config_hash = _compute_config_hash(transformed_config)
|
|
896
895
|
|
|
897
896
|
return {
|
|
898
897
|
"success": True,
|
|
@@ -1034,7 +1033,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1034
1033
|
# For existing dashboards, optionally validate config_hash and warn on large replacement
|
|
1035
1034
|
if dashboard_exists:
|
|
1036
1035
|
# Fetch current config for validation/comparison
|
|
1037
|
-
get_data
|
|
1036
|
+
get_data = {
|
|
1038
1037
|
"type": "lovelace/config",
|
|
1039
1038
|
"force": True,
|
|
1040
1039
|
}
|
|
@@ -37,7 +37,7 @@ MCP_TOOLS_REPO = "julienld/ha-mcp-test-custom-component"
|
|
|
37
37
|
MCP_TOOLS_DOMAIN = "ha_mcp_tools"
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def register_mcp_component_tools(mcp, client, **kwargs):
|
|
40
|
+
def register_mcp_component_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
41
41
|
"""Register MCP component installation tools.
|
|
42
42
|
|
|
43
43
|
This function only registers tools if the feature flag is enabled.
|
|
@@ -236,7 +236,7 @@ def register_mcp_component_tools(mcp, client, **kwargs):
|
|
|
236
236
|
],
|
|
237
237
|
))
|
|
238
238
|
|
|
239
|
-
result = {
|
|
239
|
+
result: dict[str, Any] = {
|
|
240
240
|
"success": True,
|
|
241
241
|
"installed": True,
|
|
242
242
|
"repository": MCP_TOOLS_REPO,
|
|
@@ -50,7 +50,7 @@ def _build_service_suggestions(domain: str, service: str, entity_id: str | None)
|
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def register_service_tools(mcp, client, **kwargs):
|
|
53
|
+
def register_service_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
54
54
|
"""Register service call and operation monitoring tools with the MCP server."""
|
|
55
55
|
device_tools = kwargs.get("device_tools")
|
|
56
56
|
if not device_tools:
|
|
@@ -230,7 +230,7 @@ async def _resolve_trace_item_id(
|
|
|
230
230
|
logger.debug(
|
|
231
231
|
f"Resolved {entity_id} to unique_id: {unique_id}"
|
|
232
232
|
)
|
|
233
|
-
return unique_id
|
|
233
|
+
return str(unique_id)
|
|
234
234
|
|
|
235
235
|
# Fallback to object_id if no unique_id found
|
|
236
236
|
logger.debug(
|
|
@@ -449,7 +449,7 @@ def _format_detailed_trace(
|
|
|
449
449
|
actions.append(step_info)
|
|
450
450
|
|
|
451
451
|
# Sort by timestamp (if available) or path to maintain execution order
|
|
452
|
-
def sort_key(item):
|
|
452
|
+
def sort_key(item: dict[str, Any]) -> tuple[str, str]:
|
|
453
453
|
return (item.get("timestamp", ""), item.get("path", ""))
|
|
454
454
|
|
|
455
455
|
triggers.sort(key=sort_key)
|
|
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.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/README.md
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
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/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
|
{ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/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.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/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
|