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.
- {ha_mcp_dev-7.4.1.dev444/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev446}/PKG-INFO +2 -1
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/pyproject.toml +4 -1
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/config.py +31 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/errors.py +8 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/server.py +13 -1
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/settings_ui.py +60 -29
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_bug_report.py +4 -1
- ha_mcp_dev-7.4.1.dev446/src/ha_mcp/tools/tools_code.py +1293 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/transforms/categorized_search.py +25 -2
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446/src/ha_mcp_dev.egg-info}/PKG-INFO +2 -1
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/requires.txt +1 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/py.typed +0 -0
- {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
- {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
- {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
- {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
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {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
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/usage_logger.py +0 -0
- {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
- {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
- {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
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev444 → ha_mcp_dev-7.4.1.dev446}/tests/test_constants.py +0 -0
- {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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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 = """
|
|
529
|
-
|
|
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,
|
|
882
|
+
disabled_count,
|
|
883
|
+
pinned_count,
|
|
872
884
|
)
|
|
873
885
|
|
|
874
|
-
return JSONResponse(
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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"])(
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
mcp.custom_route(f"{secret_prefix}/api/settings/
|
|
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(
|
|
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}"},
|