ha-mcp-dev 7.8.0.dev712__tar.gz → 7.8.0.dev714__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 (143) hide show
  1. {ha_mcp_dev-7.8.0.dev712/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.8.0.dev714}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/config.py +61 -1
  4. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/settings.css +7 -0
  5. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/settings.js +36 -0
  6. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/settings_ui.py +26 -2
  7. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/stdio_settings_sidecar.py +38 -11
  8. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/LICENSE +0 -0
  10. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/README.md +0 -0
  12. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/_version.py +0 -0
  17. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/auth/__init__.py +0 -0
  18. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/auth/consent_form.py +0 -0
  19. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/auth/provider.py +0 -0
  20. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/backup_manager.py +0 -0
  21. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/client/__init__.py +0 -0
  22. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/client/rest_client.py +0 -0
  23. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/client/supervisor_client.py +0 -0
  24. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/client/websocket_client.py +0 -0
  25. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/client/websocket_listener.py +0 -0
  26. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  27. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  28. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  29. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/errors.py +0 -0
  30. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/policy/__init__.py +0 -0
  31. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/policy/approval_queue.py +0 -0
  32. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/policy/evaluator.py +0 -0
  33. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/policy/handlers.py +0 -0
  34. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/policy/middleware.py +0 -0
  35. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/policy/model.py +0 -0
  36. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/policy/persistence.py +0 -0
  37. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/policy/value_sources.py +0 -0
  38. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/py.typed +0 -0
  39. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/read_only.py +0 -0
  40. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  41. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  42. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  43. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  44. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  45. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  46. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  47. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  48. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  49. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  50. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  51. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  52. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  53. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  54. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  55. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  56. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  57. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  58. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  59. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  60. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  61. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  62. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/server.py +0 -0
  63. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/smoke_test.py +0 -0
  64. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/__init__.py +0 -0
  65. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/auto_backup.py +0 -0
  66. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/backup.py +0 -0
  67. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  68. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  69. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/device_control.py +0 -0
  70. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/enhanced.py +0 -0
  71. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/helpers.py +0 -0
  72. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/reference_validator.py +0 -0
  73. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/registry.py +0 -0
  74. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  75. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  76. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  77. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  78. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  79. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  80. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  81. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  82. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  83. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  84. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_addons.py +0 -0
  85. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_areas.py +0 -0
  86. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  87. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  88. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_calendar.py +0 -0
  89. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_camera.py +0 -0
  90. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_categories.py +0 -0
  91. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_code.py +0 -0
  92. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  93. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  94. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  95. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  96. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  97. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  98. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_energy.py +0 -0
  99. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_entities.py +0 -0
  100. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  101. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_groups.py +0 -0
  102. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_hacs.py +0 -0
  103. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_history.py +0 -0
  104. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_integrations.py +0 -0
  105. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_labels.py +0 -0
  106. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  107. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_registry.py +0 -0
  108. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_resources.py +0 -0
  109. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_search.py +0 -0
  110. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_service.py +0 -0
  111. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_services.py +0 -0
  112. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_system.py +0 -0
  113. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_themes.py +0 -0
  114. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_todo.py +0 -0
  115. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_traces.py +0 -0
  116. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_updates.py +0 -0
  117. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_utility.py +0 -0
  118. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  119. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  120. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/tools_zones.py +0 -0
  121. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/util_helpers.py +0 -0
  122. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/tools/validation_middleware.py +0 -0
  123. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/transforms/__init__.py +0 -0
  124. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/transforms/categorized_search.py +0 -0
  125. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  126. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/utils/__init__.py +0 -0
  127. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/utils/config_hash.py +0 -0
  128. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/utils/data_paths.py +0 -0
  129. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/utils/domain_handlers.py +0 -0
  130. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  131. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  132. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/utils/operation_manager.py +0 -0
  133. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/utils/python_sandbox.py +0 -0
  134. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/utils/skill_loader.py +0 -0
  135. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp/utils/usage_logger.py +0 -0
  136. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  137. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  138. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  139. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  140. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  141. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/tests/__init__.py +0 -0
  142. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/tests/test_constants.py +0 -0
  143. {ha_mcp_dev-7.8.0.dev712 → ha_mcp_dev-7.8.0.dev714}/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.8.0.dev712
3
+ Version: 7.8.0.dev714
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.8.0.dev712"
7
+ version = "7.8.0.dev714"
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"
@@ -89,6 +89,13 @@ class Settings(BaseSettings):
89
89
  # WebSocket configuration (essential for async operations)
90
90
  enable_websocket: bool = Field(True, alias="ENABLE_WEBSOCKET")
91
91
 
92
+ # Settings UI sidecar (stdio mode only, #1587). 0 = pick a free
93
+ # ephemeral port at every spawn (default); 1024-65535 pins the sidecar
94
+ # to a fixed port so the settings URL/origin stays stable across
95
+ # restarts (bookmarks, browser localStorage). Read by run_main() in
96
+ # stdio_settings_sidecar.py.
97
+ sidecar_pin_port: int = Field(0, alias="HA_MCP_SIDECAR_PORT")
98
+
92
99
  # Development/Debug configuration
93
100
  debug: bool = Field(False, alias="DEBUG")
94
101
  log_level: str = Field("INFO", alias="LOG_LEVEL")
@@ -437,6 +444,37 @@ class Settings(BaseSettings):
437
444
  raise ValueError(f"Backup hint must be one of {valid_hints}")
438
445
  return v.lower()
439
446
 
447
+ @field_validator("sidecar_pin_port", mode="before")
448
+ @classmethod
449
+ def _lenient_sidecar_pin_port(cls, v: object) -> int:
450
+ """0 (default) = ephemeral port; otherwise a non-privileged port.
451
+
452
+ Lenient like the time-budget validators: an empty / unparseable /
453
+ out-of-range value falls back to 0 (ephemeral) with a warning rather
454
+ than raising, so a bad ``HA_MCP_SIDECAR_PORT`` can never crash the MCP
455
+ server or the best-effort settings sidecar.
456
+ """
457
+ if isinstance(v, str):
458
+ v = v.strip()
459
+ if not v:
460
+ return 0
461
+ # bool is an int subclass but never a meaningful port; reject it
462
+ # along with anything that isn't int/str-parseable.
463
+ if v is None or isinstance(v, bool) or not isinstance(v, int | str):
464
+ logger.warning("Invalid HA_MCP_SIDECAR_PORT=%r; using ephemeral port", v)
465
+ return 0
466
+ try:
467
+ port = int(v)
468
+ except (ValueError, TypeError):
469
+ logger.warning("Invalid HA_MCP_SIDECAR_PORT=%r; using ephemeral port", v)
470
+ return 0
471
+ if port != 0 and not 1024 <= port <= 65535:
472
+ logger.warning(
473
+ "HA_MCP_SIDECAR_PORT=%r outside 1024-65535; using ephemeral port", v
474
+ )
475
+ return 0
476
+ return port
477
+
440
478
  model_config = SettingsConfigDict(
441
479
  env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="allow"
442
480
  )
@@ -505,6 +543,7 @@ AdvancedSection = Literal[
505
543
  "operations",
506
544
  "diagnostics",
507
545
  "tools_surface",
546
+ "sidecar",
508
547
  "beta_codemode",
509
548
  ]
510
549
 
@@ -728,6 +767,10 @@ ADVANCED_SETTINGS_FIELDS: tuple[AdvancedField, ...] = (
728
767
  AdvancedField("environment", "ENVIRONMENT", str, "diagnostics", True),
729
768
  AdvancedField("log_level", "LOG_LEVEL", str, "diagnostics", True),
730
769
  AdvancedField("debug", "DEBUG", bool, "diagnostics", True),
770
+ # Settings UI sidecar (stdio-only). Pin the sidecar's port so the
771
+ # settings URL/origin is stable across restarts; 0 = ephemeral
772
+ # (default). #1587.
773
+ AdvancedField("sidecar_pin_port", "HA_MCP_SIDECAR_PORT", int, "sidecar", True),
731
774
  # NOTE: ``auto_backup_dir`` and ``auto_backup_calendar_lookahead_days``
732
775
  # are NOT in this tuple. They are in ``BACKUP_OVERRIDE_FIELDS`` (defined
733
776
  # below) so they persist to ``backup_settings.json`` alongside the
@@ -778,6 +821,18 @@ _ADVANCED_SETTINGS_BOUNDS: dict[str, tuple[float, float]] = {
778
821
  "code_mode_max_memory": (1_048_576, 268_435_456),
779
822
  "code_mode_max_recursion": (1, 10_000),
780
823
  "code_mode_max_invocations": (1, 10_000),
824
+ # 0 is the "off" sentinel (ephemeral); the range below is the valid
825
+ # PINNED range. See _ADVANCED_SETTINGS_SENTINELS.
826
+ "sidecar_pin_port": (1024, 65535),
827
+ }
828
+
829
+
830
+ # Fields where a specific value is a valid "off" sentinel that bypasses the
831
+ # _ADVANCED_SETTINGS_BOUNDS range (sidecar_pin_port: 0 = ephemeral). The UI
832
+ # emits min=sentinel so the number input can still express "off"; the
833
+ # override-apply and UI-POST paths accept the sentinel OR the bounded range.
834
+ _ADVANCED_SETTINGS_SENTINELS: dict[str, int] = {
835
+ "sidecar_pin_port": 0,
781
836
  }
782
837
 
783
838
 
@@ -1159,7 +1214,12 @@ def _apply_advanced_overrides(settings: "Settings") -> None:
1159
1214
  continue
1160
1215
 
1161
1216
  bounds = _ADVANCED_SETTINGS_BOUNDS.get(fname)
1162
- if bounds is not None and not (bounds[0] <= coerced <= bounds[1]):
1217
+ sentinel = _ADVANCED_SETTINGS_SENTINELS.get(fname)
1218
+ if (
1219
+ bounds is not None
1220
+ and coerced != sentinel
1221
+ and not (bounds[0] <= coerced <= bounds[1])
1222
+ ):
1163
1223
  logger.warning(
1164
1224
  "Advanced override for %r is %s, outside %s-%s — ignoring.",
1165
1225
  fname,
@@ -284,6 +284,13 @@
284
284
  .adv-save-btn:disabled { opacity: 0.55; cursor: not-allowed;
285
285
  box-shadow: none; }
286
286
  .adv-section { border-top: 1px solid var(--border); }
287
+ /* Sidecar section greys out in non-stdio deployments (no sidecar to pin),
288
+ mirroring the .feature-row.*-sub.dimmed treatment. */
289
+ .adv-section.dimmed { opacity: 0.55; }
290
+ .adv-section.dimmed input, .adv-section.dimmed select { cursor: not-allowed; }
291
+ .adv-section-title.dimmed { opacity: 0.55; }
292
+ .adv-section-note { font-size: 0.72rem; color: var(--text-secondary);
293
+ margin: 4px 0 8px; line-height: 1.4; }
287
294
  .adv-row { display: flex; align-items: flex-start; justify-content: space-between;
288
295
  gap: 12px; padding: 10px 0; border-top: 1px solid var(--border); }
289
296
  .adv-row:first-child { border-top: none; }
@@ -2740,6 +2740,7 @@ const ADVANCED_FIELD_META = {
2740
2740
  code_mode_max_recursion: { label: "Code-mode max recursion", help: "Recursion-depth cap per sandbox run. Restart required." },
2741
2741
  code_mode_max_invocations: { label: "Code-mode max invocations", help: "API/tool-call cap per sandbox run. Restart required." },
2742
2742
  code_mode_saved_tools_path:{ label: "Saved-tools path", help: "JSON file where ha_manage_custom_tool persists saved tools across restarts. Restart required." },
2743
+ sidecar_pin_port: { label: "Settings UI sidecar port", help: "0 = a new free port each restart (default); set 1024–65535 to pin a fixed port so the settings URL stays stable across restarts. Falls back to a free port if the pinned one is busy. Restart required." },
2743
2744
  };
2744
2745
 
2745
2746
  // Fields that require an MCP-host restart to take effect when changed
@@ -2765,6 +2766,9 @@ const ADVANCED_RESTART_REQUIRED = new Set([
2765
2766
  "code_mode_max_duration", "code_mode_max_memory",
2766
2767
  "code_mode_max_recursion", "code_mode_max_invocations",
2767
2768
  "code_mode_saved_tools_path",
2769
+ // The sidecar binds its port once at spawn (run_main), so changing the
2770
+ // pin needs a restart to respawn the sidecar on the new port.
2771
+ "sidecar_pin_port",
2768
2772
  ]);
2769
2773
 
2770
2774
  let _advancedFields = [];
@@ -2825,6 +2829,8 @@ async function loadAdvancedSettings() {
2825
2829
  renderAdvancedSection('advOperations', bySection.operations || []);
2826
2830
  renderAdvancedSection('advToolsSurface', bySection.tools_surface || []);
2827
2831
  renderAdvancedSection('advDiagnostics', bySection.diagnostics || []);
2832
+ renderAdvancedSection('advSidecar', bySection.sidecar || []);
2833
+ applySidecarAvailability(data.is_stdio !== false);
2828
2834
  document.getElementById('advSaveRow').style.display = '';
2829
2835
  const topRow = document.getElementById('advSaveRowTop');
2830
2836
  if (topRow) topRow.style.display = '';
@@ -2840,6 +2846,36 @@ async function loadAdvancedSettings() {
2840
2846
  }
2841
2847
  }
2842
2848
 
2849
+ function applySidecarAvailability(isStdio) {
2850
+ // The sidecar-port setting only applies when this settings page is served
2851
+ // by the stdio settings-UI sidecar. In HTTP/SSE/OAuth/addon deployments
2852
+ // there is no sidecar, so dim + disable the section and explain why, rather
2853
+ // than letting a user save a value that does nothing.
2854
+ // Remove any note from a prior load first, so the <h3> title is once again
2855
+ // the section's previousElementSibling (an injected note would otherwise
2856
+ // shadow it on a re-render).
2857
+ const prevNote = document.getElementById('advSidecarNote');
2858
+ if (prevNote) prevNote.remove();
2859
+ const section = document.getElementById('advSidecar');
2860
+ if (!section) return;
2861
+ const title = section.previousElementSibling;
2862
+ if (isStdio) {
2863
+ section.classList.remove('dimmed');
2864
+ if (title) title.classList.remove('dimmed');
2865
+ return;
2866
+ }
2867
+ section.classList.add('dimmed');
2868
+ if (title) title.classList.add('dimmed');
2869
+ section.querySelectorAll('input, select').forEach((el) => { el.disabled = true; });
2870
+ const note = document.createElement('div');
2871
+ note.id = 'advSidecarNote';
2872
+ note.className = 'adv-section-note';
2873
+ note.textContent =
2874
+ 'Available in stdio mode only. This server runs over HTTP, which has '
2875
+ + 'no settings-UI sidecar to pin.';
2876
+ section.parentNode.insertBefore(note, section);
2877
+ }
2878
+
2843
2879
  function renderAdvancedSection(containerId, fields) {
2844
2880
  const el = document.getElementById(containerId);
2845
2881
  if (!el) return;
@@ -902,6 +902,8 @@ _SETTINGS_HTML = (
902
902
  <div id="advToolsSurface" class="adv-section"></div>
903
903
  <h3 class="adv-section-title">Diagnostics</h3>
904
904
  <div id="advDiagnostics" class="adv-section"></div>
905
+ <h3 class="adv-section-title">Settings UI sidecar</h3>
906
+ <div id="advSidecar" class="adv-section"></div>
905
907
 
906
908
  <!-- Beta features sit at the bottom of the panel — these can damage
907
909
  the HA system, so they come last and the user sees safer
@@ -2650,6 +2652,7 @@ def build_settings_handlers(
2650
2652
  from .config import (
2651
2653
  _ADVANCED_SETTINGS_BOUNDS,
2652
2654
  _ADVANCED_SETTINGS_CHOICES,
2655
+ _ADVANCED_SETTINGS_SENTINELS,
2653
2656
  ADVANCED_SETTINGS_FIELDS,
2654
2657
  OAUTH_MODE_TOKEN,
2655
2658
  _read_feature_flag_override_file,
@@ -2692,11 +2695,26 @@ def build_settings_handlers(
2692
2695
  bounds = _ADVANCED_SETTINGS_BOUNDS.get(fname)
2693
2696
  if bounds is not None:
2694
2697
  row["min"], row["max"] = bounds
2698
+ # Sentinel fields (e.g. sidecar_pin_port: 0 = off) need the
2699
+ # number input to reach below the bounded range, so expose
2700
+ # the sentinel as the UI minimum.
2701
+ sentinel = _ADVANCED_SETTINGS_SENTINELS.get(fname)
2702
+ if sentinel is not None:
2703
+ row["min"] = sentinel
2695
2704
  choices = _ADVANCED_SETTINGS_CHOICES.get(fname)
2696
2705
  if choices is not None:
2697
2706
  row["choices"] = list(choices)
2698
2707
  fields.append(row)
2699
- return JSONResponse({"fields": fields, "is_addon": is_running_in_addon()})
2708
+ # is_stdio: the sidecar-port field only applies when this settings
2709
+ # page is served by the stdio settings-UI sidecar. In HTTP/SSE/OAuth
2710
+ # /addon deployments there is no sidecar, so the UI greys the section.
2711
+ return JSONResponse(
2712
+ {
2713
+ "fields": fields,
2714
+ "is_addon": is_running_in_addon(),
2715
+ "is_stdio": get_http_settings_prefix() is None,
2716
+ }
2717
+ )
2700
2718
 
2701
2719
  def _origin_for_advanced_field(
2702
2720
  env_name: str, overrides: dict[str, Any] | None = None
@@ -2770,6 +2788,7 @@ def build_settings_handlers(
2770
2788
  from .config import (
2771
2789
  _ADVANCED_SETTINGS_BOUNDS,
2772
2790
  _ADVANCED_SETTINGS_CHOICES,
2791
+ _ADVANCED_SETTINGS_SENTINELS,
2773
2792
  _FEATURE_FLAG_OVERRIDE_FILENAME,
2774
2793
  ADVANCED_SETTINGS_FIELDS,
2775
2794
  )
@@ -2890,7 +2909,12 @@ def build_settings_handlers(
2890
2909
  return _bad_advanced_type(fname, ftype, raw)
2891
2910
 
2892
2911
  bounds = _ADVANCED_SETTINGS_BOUNDS.get(fname)
2893
- if bounds is not None and not (bounds[0] <= coerced <= bounds[1]):
2912
+ sentinel = _ADVANCED_SETTINGS_SENTINELS.get(fname)
2913
+ if (
2914
+ bounds is not None
2915
+ and coerced != sentinel
2916
+ and not (bounds[0] <= coerced <= bounds[1])
2917
+ ):
2894
2918
  return JSONResponse(
2895
2919
  create_error_response(
2896
2920
  ErrorCode.VALIDATION_INVALID_PARAMETER,
@@ -266,19 +266,40 @@ def _spawn_lock() -> Iterator[bool]:
266
266
  os.close(fd)
267
267
 
268
268
 
269
- def _pick_free_port() -> int:
270
- """Bind a transient socket to an ephemeral port and return it.
271
-
272
- The socket is closed before the sidecar opens its own listener.
273
- The OS may hand out the same port again to the sidecar; if another
274
- process snatches it in the gap, the sidecar startup will fail and
275
- log to ``sidecar.log`` the parent moves on (settings UI is
276
- advisory, not required for MCP operation).
269
+ def _pick_free_port(pinned: int = 0) -> int:
270
+ """Return the port the sidecar listener should bind.
271
+
272
+ ``pinned == 0`` (default): bind a transient socket to an ephemeral
273
+ port and return it. The socket is closed before the sidecar opens its
274
+ own listener. The OS may hand out the same port again; if another
275
+ process snatches it in the gap, the sidecar startup will fail and log
276
+ to ``sidecar.log`` the parent moves on (settings UI is advisory, not
277
+ required for MCP operation).
278
+
279
+ ``pinned != 0``: try to reserve the requested fixed port with
280
+ ``SO_REUSEADDR`` so the settings URL/origin survives restarts (#1587).
281
+ ``SO_REUSEADDR`` lets the bind succeed even if a crashed sidecar left
282
+ the port in ``TIME_WAIT``. If the port is genuinely unavailable (in
283
+ use by another process), log a warning and fall back to an ephemeral
284
+ port rather than failing the sidecar.
277
285
  """
286
+ if pinned:
287
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
288
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
289
+ try:
290
+ sock.bind(("127.0.0.1", pinned))
291
+ return int(sock.getsockname()[1])
292
+ except OSError as exc:
293
+ logger.warning(
294
+ "Sidecar pin port %d unavailable (%s); "
295
+ "falling back to an ephemeral port",
296
+ pinned,
297
+ exc,
298
+ )
299
+ return _pick_free_port(0)
278
300
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
279
301
  sock.bind(("127.0.0.1", 0))
280
- port: int = sock.getsockname()[1]
281
- return port
302
+ return int(sock.getsockname()[1])
282
303
 
283
304
 
284
305
  def maybe_spawn() -> None:
@@ -795,7 +816,13 @@ def run_main() -> int:
795
816
  format="%(asctime)s %(levelname)s %(name)s: %(message)s",
796
817
  )
797
818
 
798
- port = _pick_free_port()
819
+ # Effective pin port honours both the env var (HA_MCP_SIDECAR_PORT) and
820
+ # a value set via the settings UI Advanced tab (persisted to the override
821
+ # file and applied by get_global_settings). 0 = ephemeral. The lenient
822
+ # validator in config.Settings has already clamped any bad value to 0.
823
+ from .config import get_global_settings
824
+
825
+ port = _pick_free_port(get_global_settings().sidecar_pin_port)
799
826
  secret_token = secrets.token_urlsafe(16)
800
827
  secret_path = f"/private_{secret_token}"
801
828
  url = f"http://127.0.0.1:{port}{secret_path}/settings"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.8.0.dev712
3
+ Version: 7.8.0.dev714
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