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.
Files changed (94) hide show
  1. {ha_mcp_dev-6.7.2.dev255/src/ha_mcp_dev.egg-info → ha_mcp_dev-6.7.2.dev256}/PKG-INFO +1 -1
  2. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/pyproject.toml +4 -2
  3. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/__main__.py +25 -16
  4. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/auth/provider.py +3 -3
  5. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/client/rest_client.py +3 -1
  6. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/client/websocket_client.py +1 -1
  7. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/server.py +7 -7
  8. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/backup.py +4 -2
  9. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_addons.py +1 -1
  10. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_areas.py +2 -2
  11. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_automations.py +1 -1
  12. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_dashboards.py +12 -13
  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
  14. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_service.py +1 -1
  15. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_traces.py +2 -2
  16. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  17. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/LICENSE +0 -0
  18. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/MANIFEST.in +0 -0
  19. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/README.md +0 -0
  20. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/setup.cfg +0 -0
  21. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/__init__.py +0 -0
  22. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/_pypi_marker +0 -0
  23. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/auth/__init__.py +0 -0
  24. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/auth/consent_form.py +0 -0
  25. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/client/__init__.py +0 -0
  26. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/client/websocket_listener.py +0 -0
  27. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/config.py +0 -0
  28. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/errors.py +0 -0
  29. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/py.typed +0 -0
  30. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/card_types.json +0 -0
  31. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/dashboard_guide.md +0 -0
  32. {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
  33. {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
  34. {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
  35. {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
  36. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  37. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  38. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  39. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  40. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  41. {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
  42. {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
  43. {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
  44. {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
  45. {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
  46. {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
  47. {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
  48. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/smoke_test.py +0 -0
  49. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/__init__.py +0 -0
  50. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/device_control.py +0 -0
  51. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/enhanced.py +0 -0
  52. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/helpers.py +0 -0
  53. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/registry.py +0 -0
  54. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/smart_search.py +0 -0
  55. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  56. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  57. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_calendar.py +0 -0
  58. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_camera.py +0 -0
  59. {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
  60. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  61. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_info.py +0 -0
  62. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  63. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_entities.py +0 -0
  64. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  65. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_groups.py +0 -0
  66. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_hacs.py +0 -0
  67. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_history.py +0 -0
  68. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_integrations.py +0 -0
  69. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_labels.py +0 -0
  70. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_registry.py +0 -0
  71. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_resources.py +0 -0
  72. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_search.py +0 -0
  73. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_services.py +0 -0
  74. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_system.py +0 -0
  75. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_todo.py +0 -0
  76. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_updates.py +0 -0
  77. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_utility.py +0 -0
  78. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  79. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/tools_zones.py +0 -0
  80. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/tools/util_helpers.py +0 -0
  81. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/__init__.py +0 -0
  82. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/domain_handlers.py +0 -0
  83. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  84. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/operation_manager.py +0 -0
  85. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/python_sandbox.py +0 -0
  86. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp/utils/usage_logger.py +0 -0
  87. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  88. {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
  89. {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
  90. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  91. {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
  92. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/tests/__init__.py +0 -0
  93. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/tests/test_constants.py +0 -0
  94. {ha_mcp_dev-6.7.2.dev255 → ha_mcp_dev-6.7.2.dev256}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 6.7.2.dev255
3
+ Version: 6.7.2.dev256
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -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.dev255"
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.11"
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 typing import Any # noqa: E402
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(client=proxy_client)
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="invalid_client",
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
- "server_error",
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.get)
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: dict[str, Any] = {
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: dict[str, Any] = {
383
+ message = {
384
384
  "type": "config/floor_registry/create",
385
385
  "name": name,
386
386
  }
@@ -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]:
@@ -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 - Used to check availability, re-imported in function
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
- resources_dir = pkg_resources.files("ha_mcp") / "resources"
72
- if hasattr(resources_dir, "__fspath__"):
73
- return Path(str(resources_dir))
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: dict[str, Any] = {
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: dict[str, Any] = {"type": "lovelace/config", "force": True}
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
- transformed_config, error = _apply_jq_transform(
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: dict[str, Any] = {
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: dict[str, Any] = {
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 6.7.2.dev255
3
+ Version: 6.7.2.dev256
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT