ha-mcp-dev 7.4.1.dev444__tar.gz → 7.4.1.dev446__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 (108) hide show
  1. {ha_mcp_dev-7.4.1.dev444/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev446}/PKG-INFO +2 -1
  2. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/pyproject.toml +4 -1
  3. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/config.py +31 -0
  4. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/errors.py +8 -0
  5. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/server.py +13 -1
  6. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/settings_ui.py +60 -29
  7. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_bug_report.py +4 -1
  8. ha_mcp_dev-7.4.1.dev446/src/ha_mcp/tools/tools_code.py +1293 -0
  9. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/transforms/categorized_search.py +25 -2
  10. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446/src/ha_mcp_dev.egg-info}/PKG-INFO +2 -1
  11. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  12. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/requires.txt +1 -0
  13. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/LICENSE +0 -0
  14. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/MANIFEST.in +0 -0
  15. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/README.md +0 -0
  16. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/setup.cfg +0 -0
  17. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/__init__.py +0 -0
  18. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/__main__.py +0 -0
  19. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/_pypi_marker +0 -0
  20. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/_version.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/auth/__init__.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/auth/consent_form.py +0 -0
  23. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/auth/provider.py +0 -0
  24. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/__init__.py +0 -0
  25. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/rest_client.py +0 -0
  26. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/websocket_client.py +0 -0
  27. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/websocket_listener.py +0 -0
  28. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/py.typed +0 -0
  29. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  30. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  31. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  32. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  33. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  34. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  35. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  36. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  37. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  38. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  39. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  40. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  41. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  42. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  43. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  44. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  45. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  46. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  47. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  48. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  49. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/smoke_test.py +0 -0
  50. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/__init__.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/backup.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/device_control.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/enhanced.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/helpers.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/reference_validator.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/registry.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/smart_search.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_addons.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_areas.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_calendar.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_camera.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_categories.py +0 -0
  65. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  66. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_energy.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_entities.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_groups.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_hacs.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_history.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_integrations.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_labels.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_registry.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_resources.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_search.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_service.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_services.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_system.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_todo.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_traces.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_updates.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_utility.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_zones.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/util_helpers.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/transforms/__init__.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/__init__.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/config_hash.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/data_paths.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/domain_handlers.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  99. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  100. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/operation_manager.py +0 -0
  101. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/python_sandbox.py +0 -0
  102. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/usage_logger.py +0 -0
  103. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  104. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  105. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  106. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/tests/__init__.py +0 -0
  107. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/tests/test_constants.py +0 -0
  108. {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/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.4.1.dev444
3
+ Version: 7.4.1.dev446
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
@@ -25,6 +25,7 @@ Requires-Dist: python-dotenv==1.2.2
25
25
  Requires-Dist: truststore==0.10.4
26
26
  Requires-Dist: websockets==16.0
27
27
  Requires-Dist: cryptography==47.0.0
28
+ Requires-Dist: pydantic-monty==0.0.9
28
29
  Dynamic: license-file
29
30
 
30
31
  > **Breaking change (v7.3.0):** `ha_config_set_yaml` has been moved to [beta](docs/beta.md).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.4.1.dev444"
7
+ version = "7.4.1.dev446"
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"
@@ -31,6 +31,7 @@ dependencies = [
31
31
  "truststore==0.10.4",
32
32
  "websockets==16.0",
33
33
  "cryptography==47.0.0",
34
+ "pydantic-monty==0.0.9",
34
35
  ]
35
36
 
36
37
  [project.urls]
@@ -76,6 +77,8 @@ explicit_package_bases = true
76
77
  module = [
77
78
  "fastmcp.*",
78
79
  "jq",
80
+ "pydantic_monty",
81
+ "pydantic_monty.*",
79
82
  ]
80
83
  ignore_missing_imports = true
81
84
 
@@ -113,6 +113,37 @@ class Settings(BaseSettings):
113
113
  # supervisor UI rejects out-of-range values before they reach env vars.
114
114
  tool_search_max_results: int = Field(5, ge=2, le=10, alias="TOOL_SEARCH_MAX_RESULTS")
115
115
 
116
+ # Code Mode — sandboxed Python execution via pydantic-monty.
117
+ # Provides an "escape hatch" tool (ha_manage_custom_tool) that lets LLMs write
118
+ # custom one-off Python code when no existing tool covers the request.
119
+ # Disabled by default due to the inherent risk of LLM-generated code.
120
+ # Range bounds reject zero/negative values that would silently break the
121
+ # tool and clamp upper bounds at sane safety margins (5 min wall-clock,
122
+ # 256 MB memory, 10k recursion, 10k API/tool calls per execution).
123
+ enable_code_mode: bool = Field(False, alias="ENABLE_CODE_MODE")
124
+ code_mode_max_duration: float = Field(
125
+ 30.0, ge=1.0, le=300.0, alias="CODE_MODE_MAX_DURATION"
126
+ )
127
+ code_mode_max_memory: int = Field(
128
+ 10_485_760, ge=1_048_576, le=268_435_456, alias="CODE_MODE_MAX_MEMORY"
129
+ ) # 10 MB default; 1 MB floor, 256 MB ceiling
130
+ code_mode_max_recursion: int = Field(
131
+ 100, ge=1, le=10_000, alias="CODE_MODE_MAX_RECURSION"
132
+ )
133
+ code_mode_max_invocations: int = Field(
134
+ 100, ge=1, le=10_000, alias="CODE_MODE_MAX_INVOCATIONS"
135
+ )
136
+ # Path to a JSON file for persisting saved custom tools across restarts.
137
+ # Empty string disables persistence (saved tools live in process memory
138
+ # and are lost on restart). The addon sets this to /data/saved_tools.json
139
+ # by default so saved tools survive addon restarts (the /data directory
140
+ # is mapped per-addon by Supervisor and is preserved across addon
141
+ # updates).
142
+ code_mode_saved_tools_path: str = Field(
143
+ "", alias="CODE_MODE_SAVED_TOOLS_PATH"
144
+ )
145
+
146
+
116
147
  @property
117
148
  def env_file_name(self) -> str:
118
149
  """Get the current environment file name."""
@@ -81,6 +81,14 @@ class ErrorCode(StrEnum):
81
81
  # Component errors
82
82
  COMPONENT_NOT_INSTALLED = "COMPONENT_NOT_INSTALLED"
83
83
 
84
+ # Code-mode sandbox errors. The sandbox is a separate execution
85
+ # context; runtime failures inside it map cleanly to one of these
86
+ # three buckets so the LLM can self-recover instead of seeing every
87
+ # failure as INTERNAL_ERROR.
88
+ SANDBOX_LIMIT_EXCEEDED = "SANDBOX_LIMIT_EXCEEDED"
89
+ SANDBOX_SYNTAX_UNSUPPORTED = "SANDBOX_SYNTAX_UNSUPPORTED"
90
+ SANDBOX_RUNTIME_ERROR = "SANDBOX_RUNTIME_ERROR"
91
+
84
92
 
85
93
  # Default suggestions for common error codes
86
94
  DEFAULT_SUGGESTIONS: dict[ErrorCode, list[str]] = {
@@ -590,6 +590,11 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
590
590
  )
591
591
  pinned.extend(getattr(self, "_skill_tool_names", []))
592
592
 
593
+ # Pin code mode tool so it gets individual permission gating
594
+ # rather than being hidden behind the BM25 search proxy.
595
+ if self.settings.enable_code_mode:
596
+ pinned.append("ha_manage_custom_tool")
597
+
593
598
  # The client may not support resources or server instructions — add
594
599
  # skills hint to the search tool description (the one place the LLM
595
600
  # is guaranteed to see).
@@ -609,12 +614,19 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
609
614
  max_results=self.settings.tool_search_max_results,
610
615
  always_visible=pinned,
611
616
  search_tool_description=description,
617
+ # Pinned tools must be excluded from the proxy's
618
+ # category sets when code mode is on; otherwise sandbox
619
+ # code can launder a recursive ``ha_manage_custom_tool``
620
+ # invocation through ``ha_call_write_tool``. See the
621
+ # docstring on ``_rebuild_category_cache``.
622
+ enable_code_mode=self.settings.enable_code_mode,
612
623
  )
613
624
  )
614
625
  logger.info(
615
- "Tool search transform applied (%d pinned tools, max_results=%d)",
626
+ "Tool search transform applied (%d pinned tools, max_results=%d, code_mode=%s)",
616
627
  len(pinned),
617
628
  self.settings.tool_search_max_results,
629
+ self.settings.enable_code_mode,
618
630
  )
619
631
  except Exception:
620
632
  logger.exception("Failed to apply tool search transform")
@@ -50,6 +50,7 @@ class ToolStub(TypedDict):
50
50
  destructiveHint: NotRequired[bool]
51
51
  disabled_by: NotRequired[str]
52
52
 
53
+
53
54
  _VALID_STATES = frozenset({"enabled", "disabled", "pinned"})
54
55
 
55
56
  logger = logging.getLogger(__name__)
@@ -261,7 +262,9 @@ def _render_stub(name: str, meta: ToolStub) -> dict[str, Any]:
261
262
  return rendered
262
263
 
263
264
 
264
- async def _get_tool_metadata(server: HomeAssistantSmartMCPServer) -> list[dict[str, Any]]:
265
+ async def _get_tool_metadata(
266
+ server: HomeAssistantSmartMCPServer,
267
+ ) -> list[dict[str, Any]]:
265
268
  """Extract metadata for all registered tools from the server.
266
269
 
267
270
  Uses FastMCP's internal ``local_provider._list_tools()`` because the
@@ -291,14 +294,16 @@ async def _get_tool_metadata(server: HomeAssistantSmartMCPServer) -> list[dict[s
291
294
  title = getattr(tool, "title", None) or tool.name
292
295
  if tool.annotations and getattr(tool.annotations, "title", None):
293
296
  title = tool.annotations.title
294
- tools.append({
295
- "name": tool.name,
296
- "title": title,
297
- "description": (tool.description or "")[:200],
298
- "tags": tags,
299
- "primary_tag": primary,
300
- "annotations": annotations,
301
- })
297
+ tools.append(
298
+ {
299
+ "name": tool.name,
300
+ "title": title,
301
+ "description": (tool.description or "")[:200],
302
+ "tags": tags,
303
+ "primary_tag": primary,
304
+ "annotations": annotations,
305
+ }
306
+ )
302
307
 
303
308
  registered_names = {t["name"] for t in tools}
304
309
 
@@ -362,7 +367,8 @@ def apply_tool_visibility(
362
367
  return pinned_names
363
368
 
364
369
 
365
- _SETTINGS_HTML = """\
370
+ _SETTINGS_HTML = (
371
+ """\
366
372
  <!DOCTYPE html>
367
373
  <html lang="en">
368
374
  <head>
@@ -525,8 +531,12 @@ async function restartAddon() {
525
531
  }
526
532
  }
527
533
 
528
- const DEFAULT_PINNED = """ + json.dumps(list(DEFAULT_PINNED_TOOLS)) + """;
529
- const MANDATORY = """ + json.dumps(list(MANDATORY_TOOLS)) + """;
534
+ const DEFAULT_PINNED = """
535
+ + json.dumps(list(DEFAULT_PINNED_TOOLS))
536
+ + """;
537
+ const MANDATORY = """
538
+ + json.dumps(list(MANDATORY_TOOLS))
539
+ + """;
530
540
 
531
541
  function getState(name) {
532
542
  if (toolStates[name]) return toolStates[name];
@@ -763,6 +773,7 @@ loadTools();
763
773
  </body>
764
774
  </html>
765
775
  """
776
+ )
766
777
 
767
778
 
768
779
  def register_settings_routes(
@@ -868,15 +879,18 @@ def register_settings_routes(
868
879
  pinned_count = sum(1 for s in states.values() if s == "pinned")
869
880
  logger.info(
870
881
  "Saved tool config (restart required to apply): %d disabled, %d pinned",
871
- disabled_count, pinned_count,
882
+ disabled_count,
883
+ pinned_count,
872
884
  )
873
885
 
874
- return JSONResponse({
875
- "success": True,
876
- "disabled": disabled_count,
877
- "pinned": pinned_count,
878
- "restart_required": True,
879
- })
886
+ return JSONResponse(
887
+ {
888
+ "success": True,
889
+ "disabled": disabled_count,
890
+ "pinned": pinned_count,
891
+ "restart_required": True,
892
+ }
893
+ )
880
894
 
881
895
  async def _restart_addon(_: Request) -> JSONResponse:
882
896
  token = os.environ.get("SUPERVISOR_TOKEN")
@@ -892,13 +906,20 @@ def register_settings_routes(
892
906
  # Short timeout — the supervisor kills our process during restart so
893
907
  # the connection will drop. A connection drop is actually success.
894
908
  try:
895
- async with httpx.AsyncClient(timeout=5.0) as client:
909
+ async with httpx.AsyncClient(
910
+ timeout=5.0, verify=server.settings.verify_ssl
911
+ ) as client:
896
912
  resp = await client.post(
897
913
  "http://supervisor/addons/self/restart",
898
914
  headers={"Authorization": f"Bearer {token}"},
899
915
  )
900
- except (httpx.ReadError, httpx.RemoteProtocolError, httpx.ConnectError):
901
- # Connection dropped mid-request — restart is happening
916
+ except (httpx.ReadError, httpx.RemoteProtocolError):
917
+ # Connection dropped mid-request — restart is happening.
918
+ # `ConnectError` is deliberately NOT in this tuple: it fires
919
+ # before a connection is established (DNS failure, TCP refused,
920
+ # Supervisor socket misconfigured) and means the restart was
921
+ # never initiated. Falls through to the `httpx.HTTPError`
922
+ # handler below, which returns 502 + CONNECTION_FAILED.
902
923
  logger.info("Restart request connection dropped (expected during restart)")
903
924
  return JSONResponse({"success": True, "message": "Restart initiated"})
904
925
  except httpx.HTTPError as e:
@@ -924,9 +945,11 @@ def register_settings_routes(
924
945
  return JSONResponse({"success": True, "message": "Restart initiated"})
925
946
 
926
947
  async def _settings_info(_: Request) -> JSONResponse:
927
- return JSONResponse({
928
- "is_addon": is_running_in_addon(),
929
- })
948
+ return JSONResponse(
949
+ {
950
+ "is_addon": is_running_in_addon(),
951
+ }
952
+ )
930
953
 
931
954
  secret_prefix = secret_path.rstrip("/") if secret_path else ""
932
955
  is_addon = is_running_in_addon()
@@ -958,7 +981,15 @@ def register_settings_routes(
958
981
  # endpoint. The frontend uses relative fetches (./api/settings/...)
959
982
  # so the JS works at either prefix unchanged.
960
983
  mcp.custom_route(f"{secret_prefix}/settings", methods=["GET"])(_settings_page)
961
- mcp.custom_route(f"{secret_prefix}/api/settings/tools", methods=["GET"])(_get_tools)
962
- mcp.custom_route(f"{secret_prefix}/api/settings/tools", methods=["POST"])(_save_tools)
963
- mcp.custom_route(f"{secret_prefix}/api/settings/restart", methods=["POST"])(_restart_addon)
964
- mcp.custom_route(f"{secret_prefix}/api/settings/info", methods=["GET"])(_settings_info)
984
+ mcp.custom_route(f"{secret_prefix}/api/settings/tools", methods=["GET"])(
985
+ _get_tools
986
+ )
987
+ mcp.custom_route(f"{secret_prefix}/api/settings/tools", methods=["POST"])(
988
+ _save_tools
989
+ )
990
+ mcp.custom_route(f"{secret_prefix}/api/settings/restart", methods=["POST"])(
991
+ _restart_addon
992
+ )
993
+ mcp.custom_route(f"{secret_prefix}/api/settings/info", methods=["GET"])(
994
+ _settings_info
995
+ )
@@ -19,6 +19,7 @@ from pydantic import Field
19
19
 
20
20
  from ha_mcp import __version__
21
21
 
22
+ from ..config import get_global_settings
22
23
  from ..utils.usage_logger import (
23
24
  AVG_LOG_ENTRIES_PER_TOOL,
24
25
  get_recent_logs,
@@ -181,7 +182,9 @@ async def _fetch_addon_logs() -> str:
181
182
  return ""
182
183
 
183
184
  try:
184
- async with httpx.AsyncClient(timeout=10.0) as http_client:
185
+ async with httpx.AsyncClient(
186
+ timeout=10.0, verify=get_global_settings().verify_ssl
187
+ ) as http_client:
185
188
  resp = await http_client.get(
186
189
  "http://supervisor/addons/self/logs",
187
190
  headers={"Authorization": f"Bearer {token}"},