Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4660543
feat: web-based settings UI for per-tool enable/disable/pin
kingpanther13 Apr 12, 2026
0089c27
fix: address CI failures and Gemini review comments
kingpanther13 Apr 12, 2026
915cd66
feat: add ingress config and text field fallbacks for addon
kingpanther13 Apr 12, 2026
d8623e6
fix: add ingress_stream, serve settings at root for ingress
kingpanther13 Apr 12, 2026
59c1eda
fix: add ingress + text fields to addon-dev config
kingpanther13 Apr 12, 2026
041f1b6
fix: revert homeassistant-addon/ to master (release pipeline handles it)
kingpanther13 Apr 12, 2026
1da91b2
fix: use live FastMCP list_tools() and relative fetch URLs
kingpanther13 Apr 13, 2026
0b5e8db
feat: dual toggles, feature-gated stubs, tool_search_max_results, gro…
kingpanther13 Apr 13, 2026
ad46b8a
fix: remove unused type: ignore on _list_tools
kingpanther13 Apr 13, 2026
c3beb1e
feat: preserve group open state, add per-group master toggle
kingpanther13 Apr 13, 2026
fe8440a
fix: require restart to apply tool visibility changes
kingpanther13 Apr 13, 2026
93a3e52
feat: add in-UI restart button using Supervisor API
kingpanther13 Apr 13, 2026
1b05ca9
fix: treat dropped connection as success in restart handler
kingpanther13 Apr 13, 2026
d8cc976
docs: document settings UI in addon DOCS and .env.example
kingpanther13 Apr 13, 2026
9c6e8de
fix: address Gemini review feedback
kingpanther13 Apr 13, 2026
c44b323
fix: remove unnecessary type annotation quotes (UP037)
kingpanther13 Apr 13, 2026
72cd3ca
Merge upstream/master into feat/settings-ui + address Patch76 review
kingpanther13 May 2, 2026
3477a39
fix: pass real FastMCP to register_settings_routes (mypy)
kingpanther13 May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ LOG_LEVEL=INFO
# top-level keys in configuration.yaml and packages/*.yaml.
# ENABLE_YAML_CONFIG_EDITING=false

# Tool visibility seed values (comma-separated tool names). On first start
# these are written to tool_config.json; after that the web settings UI at
# http://<server>/settings is the source of truth. See DOCS.md for details.
# DISABLED_TOOLS=
# PINNED_TOOLS=

# Max results from ha_search_tools (2-10, default 5).
# TOOL_SEARCH_MAX_RESULTS=5

# Optional: MCP Server Configuration
MCP_SERVER_NAME=ha-mcp
# MCP_SERVER_VERSION defaults to the package version (e.g. 6.7.2)
26 changes: 26 additions & 0 deletions homeassistant-addon-dev/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,32 @@ The dev add-on uses the same configuration as the stable version. See the main a
|--------|-------------|---------|
| `backup_hint` | Backup strength preference | `normal` |
| `secret_path` | Custom secret path (optional) | auto-generated |
| `enable_skills` | Serve bundled HA best-practice skills as MCP resources | `true` |
| `enable_skills_as_tools` | Expose skills via list_resources/read_resource tools | `false` |
| `enable_tool_search` | Replace full tool catalog with search-based discovery (~46K → ~5K tokens) | `false` |
| `enable_yaml_config_editing` | Allow `ha_config_set_yaml` to edit configuration.yaml | `false` |
| `tool_search_max_results` | Max results from `ha_search_tools` (range 2-10) | `5` |
| `disabled_tools` | Comma-separated list of tool names to disable (seed value; web UI is primary) | empty |
| `pinned_tools` | Comma-separated list of tool names to pin when tool search is enabled (seed value; web UI is primary) | empty |

## Tool Settings Web UI

The add-on exposes a web-based settings page for managing which tools are available to AI assistants. Click **"Open Web UI"** on the add-on info page to access it.

Features:
- **Enable/disable individual tools** — toggle each tool on or off
- **Pin tools** — keep tools always visible when `enable_tool_search` is on
- **Per-group master toggle** — enable/disable all tools in a group (HACS, System, etc.) with one click
- **Search** — filter tools by name or title
- **Mandatory tools** — `ha_search_entities`, `ha_get_overview`, `ha_get_state`, `ha_report_issue` are always enabled and cannot be disabled
- **Feature-gated tools** — `ha_config_set_yaml` (requires `enable_yaml_config_editing`) and filesystem tools (require `HAMCP_ENABLE_FILESYSTEM_TOOLS`) appear in the list with a note if disabled via their feature flag
- **In-UI restart** — a "Restart Add-on" button appears after saving to apply changes with one click

**Important:** Tool configuration changes require an add-on restart to take effect. The UI will prompt you to restart after saving.

### Text-field fallback

If you prefer not to use the web UI (or want to set these before first start), the `disabled_tools` and `pinned_tools` options accept comma-separated tool names as seed values. On first start, the add-on creates `/data/tool_config.json` from these values. After that, the web UI is the source of truth.

## Updates

Expand Down
9 changes: 9 additions & 0 deletions homeassistant-addon-dev/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ arch:
init: false
startup: application
boot: manual
ingress: true
ingress_port: 9583
ingress_stream: true
hassio_api: true
hassio_role: default
homeassistant_api: true
Expand All @@ -21,12 +24,18 @@ options:
enable_skills_as_tools: false
enable_tool_search: false
enable_yaml_config_editing: false
tool_search_max_results: 5
disabled_tools: ""
pinned_tools: ""
schema:
backup_hint: list(strong|normal|weak|auto)
secret_path: str?
enable_skills: bool?
enable_skills_as_tools: bool?
enable_tool_search: bool?
enable_yaml_config_editing: bool?
tool_search_max_results: int?
disabled_tools: str?
pinned_tools: str?
ports:
9583/tcp: 9583
19 changes: 19 additions & 0 deletions homeassistant-addon-dev/translations/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,22 @@ configuration:
like homeassistant, http, and recorder are blocked. A backup is
created before every edit. Use for YAML-only features that have no
UI or API alternative. Requires restart to take effect.
tool_search_max_results:
name: Tool search max results
description: >-
Maximum number of tools returned by ha_search_tools when tool
search is enabled. Lower values (2-3) save context tokens but
may miss relevant tools. Range: 2-10. Requires restart.
disabled_tools:
name: Disabled tools (text fallback)
description: >-
Comma-separated tool names to disable. For a visual interface,
click "Open Web UI" on the addon info page. This field seeds the initial config
when the web UI hasn't been used yet. Requires restart.
pinned_tools:
name: Pinned tools (text fallback)
description: >-
Comma-separated tool names to pin (always visible in tool search).
For a visual interface, click "Open Web UI" on the addon info page. This field
seeds the initial config when the web UI hasn't been used yet.
Requires restart.
12 changes: 12 additions & 0 deletions homeassistant-addon/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ def main() -> int:
enable_skills_as_tools = False # default
enable_tool_search = False # default
enable_yaml_config_editing = False # default
tool_search_max_results = 5 # default
disabled_tools_raw = "" # default
pinned_tools_raw = "" # default

if config_file.exists():
try:
Expand All @@ -125,6 +128,12 @@ def main() -> int:
enable_tool_search = raw_tool_search if isinstance(raw_tool_search, bool) else False
raw_yaml_config = config.get("enable_yaml_config_editing", False)
enable_yaml_config_editing = raw_yaml_config if isinstance(raw_yaml_config, bool) else False
raw_max_results = config.get("tool_search_max_results", 5)
tool_search_max_results = raw_max_results if isinstance(raw_max_results, int) else 5
raw_disabled = config.get("disabled_tools", "")
disabled_tools_raw = raw_disabled if isinstance(raw_disabled, str) else ""
raw_pinned = config.get("pinned_tools", "")
pinned_tools_raw = raw_pinned if isinstance(raw_pinned, str) else ""
except Exception as e:
log_error(f"Failed to read config: {e}, using defaults")

Expand All @@ -140,6 +149,9 @@ def main() -> int:
os.environ["ENABLE_SKILLS_AS_TOOLS"] = str(enable_skills_as_tools).lower()
os.environ["ENABLE_TOOL_SEARCH"] = str(enable_tool_search).lower()
os.environ["ENABLE_YAML_CONFIG_EDITING"] = str(enable_yaml_config_editing).lower()
os.environ["TOOL_SEARCH_MAX_RESULTS"] = str(tool_search_max_results)
os.environ["DISABLED_TOOLS"] = disabled_tools_raw
os.environ["PINNED_TOOLS"] = pinned_tools_raw

# Validate Supervisor token
supervisor_token = os.environ.get("SUPERVISOR_TOKEN")
Expand Down
9 changes: 9 additions & 0 deletions src/ha_mcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ class Settings(BaseSettings):
# files. Disabled by default; only for YAML-only features with no UI/API path.
enable_yaml_config_editing: bool = Field(False, alias="ENABLE_YAML_CONFIG_EDITING")

# Seed values for tool visibility (comma-separated tool names).
# Used as initial config when no tool_config.json exists.
# The web settings UI (/settings) is the primary interface for managing these.
disabled_tools: str = Field("", alias="DISABLED_TOOLS")
pinned_tools: str = Field("", alias="PINNED_TOOLS")

# Max results returned by ha_search_tools (2-10).
tool_search_max_results: int = Field(5, alias="TOOL_SEARCH_MAX_RESULTS")

@model_validator(mode="after")
def _skills_dependency(self) -> "Settings":
"""Auto-enable skills (resources) when skills-as-tools is on.
Expand Down
24 changes: 23 additions & 1 deletion src/ha_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ def _initialize_server(self) -> None:
# Register bundled skills as MCP resources
self._register_skills()

# Apply user-configured tool visibility (after all tools registered,
# before tool search transform wraps them)
self._apply_settings_ui()

# Apply tool search transform (must come after all tools and
# ResourcesAsTools are registered so it can wrap everything)
self._apply_tool_search()
Expand Down Expand Up @@ -306,6 +310,23 @@ def _build_skill_block(self, skill_name: str, main_file: Path) -> str | None:

return f"\n### Skill: {skill_name} ({uri})\n{description.strip()}"

def _apply_settings_ui(self) -> None:
"""Register settings web UI and apply persisted tool visibility."""
from .settings_ui import (
apply_tool_visibility,
load_tool_config,
register_settings_routes,
)

register_settings_routes(self.mcp, self)

config = load_tool_config(self.settings)
if config:
pinned = apply_tool_visibility(self.mcp, config, self.settings)
if pinned:
self._user_pinned_tools = list(pinned)
logger.info("Applied persisted tool config (%d entries)", len(config.get("tools", {})))

# Tools pinned outside the search transform for individual permission gating.
# These are always visible in list_tools() regardless of search transform.
_PINNED_TOOLS: ClassVar[list[str]] = list(DEFAULT_PINNED_TOOLS)
Expand Down Expand Up @@ -419,8 +440,9 @@ def _apply_tool_search(self) -> None:
)
return

# Build the always_visible list
# Build the always_visible list: defaults + user-configured pins
pinned = list(self._PINNED_TOOLS)
pinned.extend(getattr(self, "_user_pinned_tools", []))
Comment thread
kingpanther13 marked this conversation as resolved.
Outdated

# Pin ResourcesAsTools and skill guidance tools if skills-as-tools is enabled
if self.settings.enable_skills_as_tools:
Expand Down
Loading