ha-mcp-dev 7.12.3.dev1863__tar.gz → 7.12.3.dev1865__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 (163) hide show
  1. {ha_mcp_dev-7.12.3.dev1863/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1865}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/pyproject.toml +8 -1
  3. ha_mcp_dev-7.12.3.dev1865/src/ha_mcp/llm_exposure.py +214 -0
  4. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/server.py +8 -0
  5. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/settings_ui/__init__.py +67 -2
  6. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/settings_ui/settings.js +42 -3
  7. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  8. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  9. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/LICENSE +0 -0
  10. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/README.md +0 -0
  12. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/_version.py +0 -0
  17. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/auth/__init__.py +0 -0
  18. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/auth/consent_form.py +0 -0
  19. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/auth/provider.py +0 -0
  20. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/backup_manager.py +0 -0
  21. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/browser_landing.py +0 -0
  22. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/client/__init__.py +0 -0
  23. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/client/rest_client.py +0 -0
  24. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/client/supervisor_client.py +0 -0
  25. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/client/websocket_client.py +0 -0
  26. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/client/websocket_listener.py +0 -0
  27. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/config.py +0 -0
  28. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  29. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  30. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  31. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/errors.py +0 -0
  32. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/policy/__init__.py +0 -0
  33. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/policy/approval_queue.py +0 -0
  34. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/policy/evaluator.py +0 -0
  35. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/policy/handlers.py +0 -0
  36. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/policy/middleware.py +0 -0
  37. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/policy/model.py +0 -0
  38. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/policy/persistence.py +0 -0
  39. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/policy/value_sources.py +0 -0
  40. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/py.typed +0 -0
  41. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/read_only.py +0 -0
  42. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  43. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  44. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  45. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  46. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  47. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  48. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  49. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  50. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  51. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  52. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  53. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  54. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  55. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  56. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  57. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  58. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  59. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  60. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  61. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  62. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  63. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  64. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  65. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  66. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  67. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/settings_ui/settings.css +0 -0
  68. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/settings_ui/settings.html +0 -0
  69. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/smoke_test.py +0 -0
  70. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  71. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/__init__.py +0 -0
  72. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/auto_backup.py +0 -0
  73. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/backup.py +0 -0
  74. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  75. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  76. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/device_control.py +0 -0
  77. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/enhanced.py +0 -0
  78. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/helpers.py +0 -0
  79. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/radio/__init__.py +0 -0
  80. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/radio/base.py +0 -0
  81. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/radio/matter.py +0 -0
  82. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/radio/thread.py +0 -0
  83. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  84. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/radio/zwave.py +0 -0
  85. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/reference_validator.py +0 -0
  86. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/registry.py +0 -0
  87. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  88. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  89. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  90. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  91. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  92. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  93. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  94. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  95. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  96. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  97. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_addons.py +0 -0
  98. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_areas.py +0 -0
  99. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  100. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  101. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_calendar.py +0 -0
  102. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_camera.py +0 -0
  103. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_categories.py +0 -0
  104. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_code.py +0 -0
  105. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  106. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  107. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  108. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  109. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  110. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  111. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_dev.py +0 -0
  112. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_energy.py +0 -0
  113. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_entities.py +0 -0
  114. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  115. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_groups.py +0 -0
  116. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_hacs.py +0 -0
  117. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_history.py +0 -0
  118. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_integrations.py +0 -0
  119. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_labels.py +0 -0
  120. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  121. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_radio.py +0 -0
  122. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_registry.py +0 -0
  123. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_resources.py +0 -0
  124. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_search.py +0 -0
  125. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_service.py +0 -0
  126. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_services.py +0 -0
  127. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_system.py +0 -0
  128. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_themes.py +0 -0
  129. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_todo.py +0 -0
  130. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_traces.py +0 -0
  131. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_updates.py +0 -0
  132. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_utility.py +0 -0
  133. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  134. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  135. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/tools_zones.py +0 -0
  136. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/util_helpers.py +0 -0
  137. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/tools/validation_middleware.py +0 -0
  138. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/transforms/__init__.py +0 -0
  139. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/transforms/categorized_search.py +0 -0
  140. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  141. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/transport_security.py +0 -0
  142. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/update_check.py +0 -0
  143. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/utils/__init__.py +0 -0
  144. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/utils/config_hash.py +0 -0
  145. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/utils/data_paths.py +0 -0
  146. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/utils/domain_handlers.py +0 -0
  147. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  148. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  149. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/utils/operation_manager.py +0 -0
  150. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/utils/python_sandbox.py +0 -0
  151. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/utils/skill_loader.py +0 -0
  152. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/utils/usage_logger.py +0 -0
  153. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/visibility/__init__.py +0 -0
  154. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/visibility/model.py +0 -0
  155. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/visibility/persistence.py +0 -0
  156. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp/visibility/resolver.py +0 -0
  157. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  158. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  159. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  160. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  161. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/tests/__init__.py +0 -0
  162. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/tests/test_constants.py +0 -0
  163. {ha_mcp_dev-7.12.3.dev1863 → ha_mcp_dev-7.12.3.dev1865}/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: 7.12.3.dev1863
3
+ Version: 7.12.3.dev1865
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 = "7.12.3.dev1863"
7
+ version = "7.12.3.dev1865"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  # Upper bound widened to <3.15 so the package installs on Home Assistant's
@@ -103,6 +103,9 @@ module = [
103
103
  "awesomeversion",
104
104
  "yarl",
105
105
  "voluptuous",
106
+ # Ships with HA core at runtime (helpers.llm schema conversion, #1745);
107
+ # not installed in this dev environment.
108
+ "voluptuous_openapi",
106
109
  "jsonschema",
107
110
  "requests",
108
111
  "requests.*",
@@ -274,6 +277,10 @@ dev = [
274
277
  # Real voluptuous for the config-flow tests (the component's
275
278
  # config_flow imports it; HA provides it at runtime, tests need it here).
276
279
  "voluptuous>=0.15.2",
280
+ # Real voluptuous-openapi for the embedded e2e LLM-API test: it converts
281
+ # the REAL tool-schema catalog exactly like llm_api.py does at runtime
282
+ # (HA core provides it at runtime; unit tests stub it).
283
+ "voluptuous-openapi>=0.4.1",
277
284
  "pytest-xdist>=3.8.0",
278
285
  "requests>=2.25.0",
279
286
  "lefthook>=1.10.0",
@@ -0,0 +1,214 @@
1
+ """Per-tool exposure control for the HA conversation-agent LLM API (#1745).
2
+
3
+ The ``ha_mcp_tools`` custom component registers this server's toolset as a
4
+ Home Assistant LLM API so conversation agents (and through them Assist chat
5
+ and voice) can drive the tools. Which tools that surface offers is a separate
6
+ axis from the global enable/disable in the settings UI:
7
+
8
+ * a tool **disabled** in the settings UI is gone for every client — FastMCP
9
+ removes it from ``tools/list`` and rejects ``tools/call`` by name;
10
+ * a tool **hidden from the LLM API** (this module) stays fully available to
11
+ regular MCP clients (claude.ai, Claude Code, ...) and is only invisible to
12
+ conversation agents — in both the full-catalog and tool-search exposure
13
+ modes of the component.
14
+
15
+ The single source of truth travels **in-band**: :class:`LlmExposureMiddleware`
16
+ stamps every ``tools/list`` entry with
17
+ ``_meta.ha_mcp = {"llm_api_exposed": bool, "pinned": bool}`` so the component
18
+ (one more loopback MCP client) filters on data that can never drift from the
19
+ server's settings, with zero extra round-trips. Stamping re-reads the
20
+ persisted settings behind a short coalescing cache (2s TTL), so settings-UI
21
+ changes apply on the agent's next conversation turn without a restart.
22
+
23
+ Defaults are deny-by-default for the risky sets (owner decision, #1745):
24
+ beta-tagged tools, developer-mode tools, and the restart/reload/backup
25
+ family start hidden from conversation agents until a user explicitly enables
26
+ them in the settings UI. Everything else starts exposed — selecting the LLM
27
+ API on an agent is itself an explicit opt-in.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import logging
33
+ import time
34
+ from collections.abc import Mapping, Sequence
35
+ from typing import TYPE_CHECKING, Any
36
+
37
+ from fastmcp.server.middleware import Middleware
38
+
39
+ if TYPE_CHECKING:
40
+ from fastmcp.server.middleware import CallNext, MiddlewareContext
41
+ from fastmcp.tools import Tool
42
+ from mcp import types as mt
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ # Key under tools/list ``_meta`` (and the key inside it) the custom component
47
+ # filters on. The component treats an ABSENT stamp as "server too old for
48
+ # per-tool exposure" and falls back to its own conservative deny-list.
49
+ META_NAMESPACE = "ha_mcp"
50
+ META_EXPOSED_KEY = "llm_api_exposed"
51
+ META_PINNED_KEY = "pinned"
52
+
53
+ # Key in tool_config.json holding the user's per-tool overrides
54
+ # ({tool_name: bool}). Sparse on purpose: only tools the user explicitly
55
+ # flipped are stored, so tools added by future releases keep getting their
56
+ # DEFAULT (e.g. a new beta tool defaults hidden) instead of freezing the
57
+ # catalog shape at save time.
58
+ LLM_API_CONFIG_KEY = "llm_api"
59
+
60
+ # Tools hidden from conversation agents by default regardless of tags:
61
+ # operational hazards where an agent mistake is expensive (restart loops,
62
+ # config reloads mid-edit, backup churn). Users can still enable each in the
63
+ # settings UI.
64
+ LLM_API_DEFAULT_OFF_TOOLS: frozenset[str] = frozenset(
65
+ {
66
+ "ha_restart",
67
+ "ha_reload_core",
68
+ "ha_manage_backup",
69
+ }
70
+ )
71
+
72
+ # Developer-mode tools are hidden by default via this name prefix (covers
73
+ # ha_dev_manage_settings / ha_dev_manage_server and any future ha_dev_*).
74
+ LLM_API_DEV_TOOL_PREFIX = "ha_dev_"
75
+
76
+ # The beta gate is tag-based so future beta tools default to hidden without
77
+ # touching this module.
78
+ _BETA_TAG = "beta"
79
+
80
+ # How long a live settings read is reused before re-reading the config file.
81
+ # tools/list fires once per conversation turn per agent; the TTL only matters
82
+ # when several agents list within the same moment.
83
+ _OVERRIDES_TTL_SECONDS = 2.0
84
+
85
+
86
+ def default_llm_api_exposed(name: str, tags: Sequence[str] | set[str]) -> bool:
87
+ """Return the default LLM-API exposure for a tool (no user override)."""
88
+ if _BETA_TAG in set(tags):
89
+ return False
90
+ if name.startswith(LLM_API_DEV_TOOL_PREFIX):
91
+ return False
92
+ return name not in LLM_API_DEFAULT_OFF_TOOLS
93
+
94
+
95
+ def effective_llm_api_exposed(
96
+ name: str,
97
+ tags: Sequence[str] | set[str],
98
+ overrides: Mapping[str, Any],
99
+ ) -> bool:
100
+ """Return the effective exposure: user override if set, else the default."""
101
+ override = overrides.get(name)
102
+ if isinstance(override, bool):
103
+ return override
104
+ return default_llm_api_exposed(name, tags)
105
+
106
+
107
+ def load_llm_api_overrides() -> dict[str, bool]:
108
+ """Read the persisted per-tool overrides from tool_config.json (live)."""
109
+ # Imported here, not at module top: settings_ui imports this module for
110
+ # the defaults, so a top-level import would be circular.
111
+ from .settings_ui import load_tool_config
112
+
113
+ config = load_tool_config()
114
+ # load_tool_config returns whatever the JSON file parses to — guard the
115
+ # valid-JSON-but-not-an-object case (e.g. `[]`) instead of raising.
116
+ if not isinstance(config, dict):
117
+ return {}
118
+ raw = config.get(LLM_API_CONFIG_KEY, {})
119
+ if not isinstance(raw, dict):
120
+ return {}
121
+ return {
122
+ name: value
123
+ for name, value in raw.items()
124
+ if isinstance(name, str) and isinstance(value, bool)
125
+ }
126
+
127
+
128
+ def _pinned_tool_names() -> set[str]:
129
+ """Return the effective pinned set, matching the server's tool-search view.
130
+
131
+ Mirrors ``server.py::_apply_tool_search``'s always-visible computation:
132
+ the built-in DEFAULT_PINNED_TOOLS minus tools the user explicitly set to
133
+ plain "enabled" (an unpin), plus every tool the user set to "pinned"
134
+ (file or env). The component mirrors pinned tools directly into the
135
+ agent's tool list when the exposure mode is tool-search.
136
+ """
137
+ from .settings_ui import effective_tool_config
138
+ from .transforms import DEFAULT_PINNED_TOOLS
139
+
140
+ states = effective_tool_config().get("tools", {})
141
+ pinned = {name for name, state in states.items() if state == "pinned"}
142
+ unpinned_defaults = {
143
+ name for name in DEFAULT_PINNED_TOOLS if states.get(name) == "enabled"
144
+ }
145
+ return (set(DEFAULT_PINNED_TOOLS) - unpinned_defaults) | pinned
146
+
147
+
148
+ class LlmExposureMiddleware(Middleware):
149
+ """Stamp every listed tool with its LLM-API exposure + pinned state.
150
+
151
+ Runs after the visibility layer, so globally-disabled tools are already
152
+ absent and never carry a stamp. The stamp is additive metadata — no tool
153
+ is hidden or altered for regular MCP clients.
154
+ """
155
+
156
+ def __init__(self) -> None:
157
+ """Initialize the short-lived settings cache."""
158
+ self._cache: tuple[float, dict[str, bool], set[str]] | None = None
159
+
160
+ def _current_settings(self) -> tuple[dict[str, bool], set[str]]:
161
+ """Return (overrides, pinned) with a short TTL over the file reads."""
162
+ now = time.monotonic()
163
+ if self._cache is not None:
164
+ stamp, overrides, pinned = self._cache
165
+ if now - stamp < _OVERRIDES_TTL_SECONDS:
166
+ return overrides, pinned
167
+ try:
168
+ overrides = load_llm_api_overrides()
169
+ pinned = _pinned_tool_names()
170
+ except Exception:
171
+ # Stamping must never break tools/list for regular clients — but
172
+ # the fallback direction matters for an exposure CONTROL: pure
173
+ # defaults would re-EXPOSE tools the user explicitly hid (their
174
+ # override lives in the unreadable settings), so serve the
175
+ # last-known-good values when we have them (review finding).
176
+ # Only a failure with no prior successful read stamps pure
177
+ # defaults. Visibly, either way.
178
+ if self._cache is not None:
179
+ _stamp, overrides, pinned = self._cache
180
+ logger.warning(
181
+ "Could not read LLM-API exposure settings; keeping the "
182
+ "last known values",
183
+ exc_info=True,
184
+ )
185
+ else:
186
+ overrides, pinned = {}, set()
187
+ logger.warning(
188
+ "Could not read LLM-API exposure settings and no prior "
189
+ "read succeeded; stamping defaults",
190
+ exc_info=True,
191
+ )
192
+ self._cache = (now, overrides, pinned)
193
+ return overrides, pinned
194
+
195
+ async def on_list_tools(
196
+ self,
197
+ context: MiddlewareContext[mt.ListToolsRequest],
198
+ call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],
199
+ ) -> Sequence[Tool]:
200
+ """Stamp ``_meta.ha_mcp`` on every tool in the list result."""
201
+ tools = await call_next(context)
202
+ overrides, pinned = self._current_settings()
203
+
204
+ stamped: list[Tool] = []
205
+ for tool in tools:
206
+ meta = dict(tool.meta or {})
207
+ namespace = dict(meta.get(META_NAMESPACE) or {})
208
+ namespace[META_EXPOSED_KEY] = effective_llm_api_exposed(
209
+ tool.name, tool.tags or set(), overrides
210
+ )
211
+ namespace[META_PINNED_KEY] = tool.name in pinned
212
+ meta[META_NAMESPACE] = namespace
213
+ stamped.append(tool.model_copy(update={"meta": meta}))
214
+ return stamped
@@ -243,6 +243,14 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
243
243
 
244
244
  self.mcp.add_middleware(ToolSearchHintMiddleware())
245
245
 
246
+ # Stamp every tools/list entry with its conversation-agent LLM API
247
+ # exposure + pinned state (#1745). Additive metadata only — regular
248
+ # clients are unaffected; the ha_mcp_tools custom component filters
249
+ # what it offers to Home Assistant conversation agents on it.
250
+ from .llm_exposure import LlmExposureMiddleware
251
+
252
+ self.mcp.add_middleware(LlmExposureMiddleware())
253
+
246
254
  # Read Only Mode write blocker (discussion #1569) — always
247
255
  # installed, consults the live flag per call. Before
248
256
  # PolicyMiddleware so a write blocked by Read Only Mode never
@@ -37,6 +37,7 @@ from ..config import (
37
37
  get_global_settings,
38
38
  )
39
39
  from ..errors import ErrorCode, create_error_response
40
+ from ..llm_exposure import LLM_API_CONFIG_KEY
40
41
  from ..transforms import DEFAULT_PINNED_TOOLS, categorize_capability
41
42
  from ..utils.data_paths import get_data_dir
42
43
 
@@ -1370,14 +1371,38 @@ def build_settings_handlers(
1370
1371
  # (their write actions are blocked at call time instead). The JS
1371
1372
  # uses this to keep their toggles live while force-disabling the
1372
1373
  # other write-capable tools' rows when the mode is on.
1374
+ # Conversation-agent LLM API exposure (#1745): effective value per
1375
+ # tool (override else default) so the UI toggle renders the truth,
1376
+ # plus the raw overrides so it can tell "user-set" from "default".
1377
+ from ..llm_exposure import effective_llm_api_exposed, load_llm_api_overrides
1373
1378
  from ..read_only import READ_ONLY_EXEMPT_TOOLS
1374
1379
 
1380
+ llm_overrides = load_llm_api_overrides()
1381
+ # Feature-gated stub rows carry their primary tag but NOT the "beta"
1382
+ # tag the registered tool declares (_render_stub renders from
1383
+ # FEATURE_GATED_TOOLS metadata, whose tags are never empty) — append
1384
+ # it whenever disabled_by is set so the toggle renders
1385
+ # hidden-by-default, matching what the stamp will say once the flag
1386
+ # turns the real tool on. Every feature-gated tool is beta by
1387
+ # definition. (Review finding: a previous `or`-fallback here was dead
1388
+ # code, so beta stubs rendered as exposed.)
1389
+ llm_effective = {
1390
+ t["name"]: effective_llm_api_exposed(
1391
+ t["name"],
1392
+ [*(t.get("tags") or []), *(["beta"] if t.get("disabled_by") else [])],
1393
+ llm_overrides,
1394
+ )
1395
+ for t in tools
1396
+ }
1397
+
1375
1398
  return JSONResponse(
1376
1399
  {
1377
1400
  "tools": tools,
1378
1401
  "states": states,
1379
1402
  "env_pinned": pinned,
1380
1403
  "read_only_exempt": sorted(READ_ONLY_EXEMPT_TOOLS),
1404
+ "llm_api": llm_effective,
1405
+ "llm_api_overrides": llm_overrides,
1381
1406
  }
1382
1407
  )
1383
1408
 
@@ -1462,8 +1487,43 @@ def build_settings_handlers(
1462
1487
  name: state for name, state in states.items() if name not in env_pinned
1463
1488
  }
1464
1489
 
1490
+ # Conversation-agent LLM API exposure overrides (#1745): a sparse
1491
+ # {tool_name: bool} map, orthogonal to the states enum. Only bools
1492
+ # persist — tools the user never flipped keep tracking their
1493
+ # (deny-by-default for beta/dev/restart) defaults across releases.
1494
+ raw_llm_api = body.get("llm_api", {})
1495
+ if not isinstance(raw_llm_api, dict):
1496
+ return JSONResponse(
1497
+ create_error_response(
1498
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
1499
+ "'llm_api' must be an object mapping tool names to booleans",
1500
+ ),
1501
+ status_code=400,
1502
+ )
1503
+ llm_api_overrides = {
1504
+ name: value
1505
+ for name, value in raw_llm_api.items()
1506
+ if isinstance(name, str) and isinstance(value, bool)
1507
+ }
1508
+
1465
1509
  config = load_tool_config()
1510
+
1511
+ # The enable/disable/pin half still needs a restart to apply
1512
+ # (visibility is wired at server build); the LLM-API exposure half
1513
+ # applies live (stamped per tools/list). Only demand a restart when
1514
+ # the half that needs one actually changed. Compare with the SAME
1515
+ # default-pinned padding _get_tools applies to its response — the JS
1516
+ # posts that padded map back verbatim, so an unpadded compare would
1517
+ # flag every first save as a states change (live-found on #1745).
1518
+ def _padded(tool_states: dict[str, str]) -> dict[str, str]:
1519
+ padded = dict(tool_states)
1520
+ for name in DEFAULT_PINNED_TOOLS:
1521
+ padded.setdefault(name, "pinned")
1522
+ return padded
1523
+
1524
+ states_changed = _padded(config.get("tools", {})) != _padded(states)
1466
1525
  config["tools"] = states
1526
+ config[LLM_API_CONFIG_KEY] = llm_api_overrides
1467
1527
  if not save_tool_config(config):
1468
1528
  return JSONResponse(
1469
1529
  create_error_response(
@@ -1480,9 +1540,13 @@ def build_settings_handlers(
1480
1540
  disabled_count = sum(1 for s in states.values() if s == "disabled")
1481
1541
  pinned_count = sum(1 for s in states.values() if s == "pinned")
1482
1542
  logger.info(
1483
- "Saved tool config (restart required to apply): %d disabled, %d pinned",
1543
+ "Saved tool config (%s): %d disabled, %d pinned, %d LLM-API overrides",
1544
+ "restart required to apply"
1545
+ if states_changed
1546
+ else "LLM-API exposure applies live",
1484
1547
  disabled_count,
1485
1548
  pinned_count,
1549
+ len(llm_api_overrides),
1486
1550
  )
1487
1551
 
1488
1552
  # Same response shape as ``_save_feature_flags`` and
@@ -1496,8 +1560,9 @@ def build_settings_handlers(
1496
1560
  {
1497
1561
  "success": True,
1498
1562
  "applied": states,
1563
+ "llm_api_applied": llm_api_overrides,
1499
1564
  "mode": "file",
1500
- "restart_required": True,
1565
+ "restart_required": states_changed,
1501
1566
  }
1502
1567
  )
1503
1568
 
@@ -26,6 +26,13 @@ let toolStates = {};
26
26
  // Populated from data.env_pinned in loadTools(); read by render() to
27
27
  // lock rows and show the env-var name banner.
28
28
  let toolEnvPinned = {};
29
+ // Conversation-agent LLM API exposure (#1745). toolLlm mirrors the
30
+ // server-computed EFFECTIVE value per tool (user override, else the
31
+ // deny-by-default for beta/dev/restart tools); toolLlmOverrides holds
32
+ // only the user-set overrides and is what saveConfig persists — tools
33
+ // never flipped keep tracking their defaults across releases.
34
+ let toolLlm = {};
35
+ let toolLlmOverrides = {};
29
36
  let saveTimer = null;
30
37
  let openGroups = new Set();
31
38
 
@@ -160,6 +167,8 @@ async function loadTools() {
160
167
  toolData = data.tools || [];
161
168
  toolStates = data.states || {};
162
169
  toolEnvPinned = data.env_pinned || {};
170
+ toolLlm = data.llm_api || {};
171
+ toolLlmOverrides = data.llm_api_overrides || {};
163
172
  READ_ONLY_EXEMPT = new Set(data.read_only_exempt || []);
164
173
  // Load policy state before the first render so the "security gated"
165
174
  // toggle reflects current policy.rules. loadPolicyState() never throws
@@ -741,6 +750,14 @@ function render() {
741
750
  `<span class="slider"></span></label>` +
742
751
  `<span>security gated</span>` +
743
752
  `</div>` +
753
+ `<div class="toggle-group ${isEnabled ? '' : 'disabled-toggle'}" ` +
754
+ `title="Offer this tool to Home Assistant conversation agents through the LLM API. Applies on the agent's next message - no restart. A tool disabled above is unavailable to agents regardless.">` +
755
+ `<label class="switch"><input type="checkbox" name="tool:${escapeHtml(t.name)}:llm" data-tool="${escapeHtml(t.name)}" data-field="llm" ` +
756
+ `aria-label="${escapeHtml(title)} exposed to the conversation-agent LLM API" ` +
757
+ `${(toolLlm[t.name] !== false) ? 'checked' : ''} ${isEnabled ? '' : 'disabled'}>` +
758
+ `<span class="slider"></span></label>` +
759
+ `<span>LLM API</span>` +
760
+ `</div>` +
744
761
  `</div>`;
745
762
 
746
763
  const inputs = div.querySelectorAll('input[type="checkbox"]');
@@ -766,6 +783,16 @@ function render() {
766
783
  render();
767
784
  return;
768
785
  }
786
+ if (field === 'llm') {
787
+ // LLM-API exposure lives in its own overrides map (persisted
788
+ // alongside states by saveConfig); the effective map mirrors it
789
+ // immediately so the re-render shows the new value.
790
+ toolLlm[t.name] = e.target.checked;
791
+ toolLlmOverrides[t.name] = e.target.checked;
792
+ scheduleSave();
793
+ render();
794
+ return;
795
+ }
769
796
  const currentState = getState(t.name);
770
797
  let newState = currentState;
771
798
  if (field === 'enabled') {
@@ -816,7 +843,7 @@ async function saveConfig() {
816
843
  resp = await fetch('./api/settings/tools', {
817
844
  method: 'POST',
818
845
  headers: {'Content-Type': 'application/json'},
819
- body: JSON.stringify({states: toolStates}),
846
+ body: JSON.stringify({states: toolStates, llm_api: toolLlmOverrides}),
820
847
  });
821
848
  } catch (e) {
822
849
  // Auto-save is fire-and-forget (scheduleSave -> setTimeout); without
@@ -826,8 +853,20 @@ async function saveConfig() {
826
853
  return;
827
854
  }
828
855
  if (resp.ok) {
829
- updateStatus('Saved. Restart required.', true);
830
- markRestartRequired();
856
+ // LLM-API-exposure-only saves apply live (stamped per tools/list);
857
+ // enable/disable/pin changes still need a restart. The server tells
858
+ // us which happened.
859
+ let restartRequired = true;
860
+ try {
861
+ const saved = await resp.json();
862
+ restartRequired = saved.restart_required !== false;
863
+ } catch (e) { /* keep the conservative default */ }
864
+ if (restartRequired) {
865
+ updateStatus('Saved. Restart required.', true);
866
+ markRestartRequired();
867
+ } else {
868
+ updateStatus('Saved. LLM API exposure applies on the next agent message.', true);
869
+ }
831
870
  } else {
832
871
  // Surface the server's structured error when present (mirrors
833
872
  // saveAdvancedSettings / saveFeatureFlag) instead of a generic
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.12.3.dev1863
3
+ Version: 7.12.3.dev1865
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
@@ -10,6 +10,7 @@ src/ha_mcp/backup_manager.py
10
10
  src/ha_mcp/browser_landing.py
11
11
  src/ha_mcp/config.py
12
12
  src/ha_mcp/errors.py
13
+ src/ha_mcp/llm_exposure.py
13
14
  src/ha_mcp/py.typed
14
15
  src/ha_mcp/read_only.py
15
16
  src/ha_mcp/server.py