ha-mcp-dev 7.6.0.dev675__tar.gz → 7.6.0.dev677__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.6.0.dev675/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev677}/PKG-INFO +5 -3
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/README.md +4 -2
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/pyproject.toml +1 -1
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/settings.js +152 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/settings_ui.py +158 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/stdio_settings_sidecar.py +15 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/best_practice_checker.py +15 -5
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_automations.py +142 -108
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_filesystem.py +5 -20
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_search.py +1 -1
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_service.py +4 -1
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/domain_handlers.py +4 -1
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677/src/ha_mcp_dev.egg-info}/PKG-INFO +5 -3
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/setup.cfg +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/settings.css +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/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.6.0.
|
|
3
|
+
Version: 7.6.0.dev677
|
|
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
|
|
@@ -372,7 +372,7 @@ For comprehensive testing documentation, see **[tests/README.md](tests/README.md
|
|
|
372
372
|
|
|
373
373
|
Ha-mcp runs **locally** on your machine. Your smart home data stays on your network.
|
|
374
374
|
|
|
375
|
-
- **No telemetry today** — anonymous usage stats are a planned future feature (as of
|
|
375
|
+
- **No telemetry today** — anonymous usage stats are a planned future feature (as of June 2026); when it lands it will follow your Home Assistant analytics/telemetry setting (which you can override), announced prominently in the release notes and the web Settings UI at least one month beforehand
|
|
376
376
|
- **No personal data collection** — we never collect entity names, configs, or device data
|
|
377
377
|
- **User-controlled bug reports** — only sent with your explicit approval
|
|
378
378
|
|
|
@@ -435,13 +435,15 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
435
435
|
- **[@griffinmartin](https://github.com/griffinmartin)** — Added OpenCode (by Anomaly) as a selectable AI client in the setup wizard, with both stdio and streamable HTTP support.
|
|
436
436
|
- **[@hhopke](https://github.com/hhopke)** — Fixed addon API calls to route through HA Core ingress proxy instead of direct container connections, fixing `ha_manage_addon` proxy mode on addon installs.
|
|
437
437
|
- **[@tomwilkie](https://github.com/tomwilkie)** — JMESPath middleware exploration (#1147) whose review-time token-measurement data informed the design of #1199 and #1225.
|
|
438
|
-
- **[@SealKan](https://github.com/SealKan)** — `fields=`/`attribute_keys=` projection on six read-heavy tools (#1225), `ha_call_event` tool (#1239),
|
|
438
|
+
- **[@SealKan](https://github.com/SealKan)** — `fields=`/`attribute_keys=` projection on six read-heavy tools (#1225), `ha_call_event` tool (#1239), dashboards-list helper refactor (#1207), `for:`-field duration-math detector in the best-practice checker (#1264), persistent DCR OAuth client registrations across restarts (#1265), and issue-triage prompt token-budgeting (#1522).
|
|
439
439
|
- **[@KarelTestSpecial](https://github.com/KarelTestSpecial)** — Cached YAML instance to prevent CPU spikes during bulk edits (#1371).
|
|
440
440
|
- **[@corgan2222](https://github.com/corgan2222)** — HA brand assets for custom integration (#1317).
|
|
441
441
|
- **[@drseanwing](https://github.com/drseanwing)** — Progress emission via FastMCP `Context` in long-running tools (#1124); tool-discovery / categorized-search docs (#1123).
|
|
442
442
|
- **[@fnordpig](https://github.com/fnordpig)** — Config subentry support (#1393) and Assist pipeline management tool (#1392).
|
|
443
443
|
- **[@paul43210](https://github.com/paul43210)** — `array_patch` mode in `ha_manage_addon` for atomic GET-modify-POST (#1063).
|
|
444
444
|
- **[@L1AD](https://github.com/L1AD)** — Filed #966 proposing tool security policies; pointed to PolicyLayer's MCP-security work as prior art that inspired the predicate DSL shape.
|
|
445
|
+
- **[@nightcityblade](https://github.com/nightcityblade)** — Updated stale Home Assistant Advanced Mode references after HA 2026.6 made formerly advanced options available by default (#1533).
|
|
446
|
+
- **[@emmelutzer](https://github.com/emmelutzer)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
|
|
445
447
|
|
|
446
448
|
---
|
|
447
449
|
|
|
@@ -342,7 +342,7 @@ For comprehensive testing documentation, see **[tests/README.md](tests/README.md
|
|
|
342
342
|
|
|
343
343
|
Ha-mcp runs **locally** on your machine. Your smart home data stays on your network.
|
|
344
344
|
|
|
345
|
-
- **No telemetry today** — anonymous usage stats are a planned future feature (as of
|
|
345
|
+
- **No telemetry today** — anonymous usage stats are a planned future feature (as of June 2026); when it lands it will follow your Home Assistant analytics/telemetry setting (which you can override), announced prominently in the release notes and the web Settings UI at least one month beforehand
|
|
346
346
|
- **No personal data collection** — we never collect entity names, configs, or device data
|
|
347
347
|
- **User-controlled bug reports** — only sent with your explicit approval
|
|
348
348
|
|
|
@@ -405,13 +405,15 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
405
405
|
- **[@griffinmartin](https://github.com/griffinmartin)** — Added OpenCode (by Anomaly) as a selectable AI client in the setup wizard, with both stdio and streamable HTTP support.
|
|
406
406
|
- **[@hhopke](https://github.com/hhopke)** — Fixed addon API calls to route through HA Core ingress proxy instead of direct container connections, fixing `ha_manage_addon` proxy mode on addon installs.
|
|
407
407
|
- **[@tomwilkie](https://github.com/tomwilkie)** — JMESPath middleware exploration (#1147) whose review-time token-measurement data informed the design of #1199 and #1225.
|
|
408
|
-
- **[@SealKan](https://github.com/SealKan)** — `fields=`/`attribute_keys=` projection on six read-heavy tools (#1225), `ha_call_event` tool (#1239),
|
|
408
|
+
- **[@SealKan](https://github.com/SealKan)** — `fields=`/`attribute_keys=` projection on six read-heavy tools (#1225), `ha_call_event` tool (#1239), dashboards-list helper refactor (#1207), `for:`-field duration-math detector in the best-practice checker (#1264), persistent DCR OAuth client registrations across restarts (#1265), and issue-triage prompt token-budgeting (#1522).
|
|
409
409
|
- **[@KarelTestSpecial](https://github.com/KarelTestSpecial)** — Cached YAML instance to prevent CPU spikes during bulk edits (#1371).
|
|
410
410
|
- **[@corgan2222](https://github.com/corgan2222)** — HA brand assets for custom integration (#1317).
|
|
411
411
|
- **[@drseanwing](https://github.com/drseanwing)** — Progress emission via FastMCP `Context` in long-running tools (#1124); tool-discovery / categorized-search docs (#1123).
|
|
412
412
|
- **[@fnordpig](https://github.com/fnordpig)** — Config subentry support (#1393) and Assist pipeline management tool (#1392).
|
|
413
413
|
- **[@paul43210](https://github.com/paul43210)** — `array_patch` mode in `ha_manage_addon` for atomic GET-modify-POST (#1063).
|
|
414
414
|
- **[@L1AD](https://github.com/L1AD)** — Filed #966 proposing tool security policies; pointed to PolicyLayer's MCP-security work as prior art that inspired the predicate DSL shape.
|
|
415
|
+
- **[@nightcityblade](https://github.com/nightcityblade)** — Updated stale Home Assistant Advanced Mode references after HA 2026.6 made formerly advanced options available by default (#1533).
|
|
416
|
+
- **[@emmelutzer](https://github.com/emmelutzer)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
|
|
415
417
|
|
|
416
418
|
---
|
|
417
419
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.6.0.
|
|
7
|
+
version = "7.6.0.dev677"
|
|
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"
|
|
@@ -888,6 +888,149 @@ async function saveBackupConfig() {
|
|
|
888
888
|
}
|
|
889
889
|
}
|
|
890
890
|
|
|
891
|
+
// ---- Custom filesystem directories (issue #1567) ---------------------------
|
|
892
|
+
// The list lives in the ha_mcp_tools custom component; the GET/POST endpoints
|
|
893
|
+
// proxy to it via authenticated HA service calls. Cached so the sub-form
|
|
894
|
+
// re-renders synchronously when the beta master / filesystem-tools toggle
|
|
895
|
+
// flips, without re-fetching.
|
|
896
|
+
// Consumed fields of the GET response: {available, paths, deny_floor, reason}
|
|
897
|
+
// (the endpoint also returns builtin_read_dirs/builtin_write_dirs, unused here).
|
|
898
|
+
let _fsCustomPathsData = null;
|
|
899
|
+
|
|
900
|
+
async function loadFsCustomPaths() {
|
|
901
|
+
try {
|
|
902
|
+
const resp = await fetch('./api/settings/fs-custom-paths');
|
|
903
|
+
if (!resp.ok) {
|
|
904
|
+
_fsCustomPathsData = {
|
|
905
|
+
available: false,
|
|
906
|
+
reason: `HTTP ${resp.status}`,
|
|
907
|
+
paths: [],
|
|
908
|
+
deny_floor: [],
|
|
909
|
+
};
|
|
910
|
+
} else {
|
|
911
|
+
_fsCustomPathsData = await resp.json();
|
|
912
|
+
}
|
|
913
|
+
} catch (err) {
|
|
914
|
+
_fsCustomPathsData = {
|
|
915
|
+
available: false,
|
|
916
|
+
reason: 'Network error: ' + String(err),
|
|
917
|
+
paths: [],
|
|
918
|
+
deny_floor: [],
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
// Re-render the feature panel so the sub-form reflects the loaded data.
|
|
922
|
+
if (Object.keys(_lastFeatureFlags).length) renderFeatureFlags(_lastFeatureFlags);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Injected beneath the enable_filesystem_tools row in renderFeatureFlags.
|
|
926
|
+
// Second-level nested (under filesystem tools, itself beta-sub-nested under
|
|
927
|
+
// the master), dimmed when either the master beta or filesystem tools is off.
|
|
928
|
+
function renderFsCustomPathsSubForm(parentEl, masterOn, fsOn) {
|
|
929
|
+
const lockedByGate = !masterOn || !fsOn;
|
|
930
|
+
const d = _fsCustomPathsData;
|
|
931
|
+
const row = document.createElement('div');
|
|
932
|
+
row.className =
|
|
933
|
+
'feature-row fs-custom-paths-sub' + (lockedByGate ? ' dimmed' : '');
|
|
934
|
+
|
|
935
|
+
const info = document.createElement('div');
|
|
936
|
+
info.className = 'feature-info';
|
|
937
|
+
const denyList =
|
|
938
|
+
d && Array.isArray(d.deny_floor) && d.deny_floor.length
|
|
939
|
+
? d.deny_floor.join(', ')
|
|
940
|
+
: '.storage, secrets.yaml';
|
|
941
|
+
info.innerHTML =
|
|
942
|
+
`<div class="feature-name">Custom filesystem directories (advanced)</div>` +
|
|
943
|
+
`<div class="feature-help">Extra directories (one per line, relative to your config dir) that the file tools may READ and WRITE — e.g. <code>pyscript</code>, <code>python_scripts</code>. Each entry grants both read and write. Applies immediately; no restart needed.</div>` +
|
|
944
|
+
`<div class="feature-help">Always blocked (cannot be added): <code>${escapeHtml(denyList)}</code>, path traversal (<code>..</code>), and absolute paths.</div>`;
|
|
945
|
+
|
|
946
|
+
const control = document.createElement('div');
|
|
947
|
+
control.className = 'feature-control';
|
|
948
|
+
|
|
949
|
+
if (lockedByGate) {
|
|
950
|
+
const note = document.createElement('div');
|
|
951
|
+
note.className = 'feature-locked-note';
|
|
952
|
+
note.textContent =
|
|
953
|
+
'Enable beta features and filesystem tools above to edit.';
|
|
954
|
+
control.appendChild(note);
|
|
955
|
+
} else if (!d) {
|
|
956
|
+
const note = document.createElement('div');
|
|
957
|
+
note.className = 'feature-help';
|
|
958
|
+
note.textContent = 'Loading…';
|
|
959
|
+
control.appendChild(note);
|
|
960
|
+
} else if (!d.available) {
|
|
961
|
+
const note = document.createElement('div');
|
|
962
|
+
note.className = 'feature-locked-note';
|
|
963
|
+
note.textContent =
|
|
964
|
+
d.reason || 'Custom directories are currently unavailable.';
|
|
965
|
+
control.appendChild(note);
|
|
966
|
+
} else {
|
|
967
|
+
const ta = document.createElement('textarea');
|
|
968
|
+
ta.id = 'fsCustomPathsInput';
|
|
969
|
+
ta.rows = 4;
|
|
970
|
+
ta.value = (d.paths || []).join('\n');
|
|
971
|
+
const btn = document.createElement('button');
|
|
972
|
+
btn.id = 'fsCustomPathsSave';
|
|
973
|
+
btn.className = 'adv-save-btn';
|
|
974
|
+
btn.textContent = 'Save directories';
|
|
975
|
+
btn.addEventListener('click', saveFsCustomPaths);
|
|
976
|
+
const status = document.createElement('div');
|
|
977
|
+
status.id = 'fsCustomPathsStatus';
|
|
978
|
+
status.className = 'feature-help';
|
|
979
|
+
control.appendChild(ta);
|
|
980
|
+
control.appendChild(btn);
|
|
981
|
+
control.appendChild(status);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
row.appendChild(info);
|
|
985
|
+
row.appendChild(control);
|
|
986
|
+
parentEl.appendChild(row);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function saveFsCustomPaths() {
|
|
990
|
+
const ta = document.getElementById('fsCustomPathsInput');
|
|
991
|
+
const btn = document.getElementById('fsCustomPathsSave');
|
|
992
|
+
const statusEl = document.getElementById('fsCustomPathsStatus');
|
|
993
|
+
if (!ta || !btn || !statusEl) return;
|
|
994
|
+
const paths = ta.value
|
|
995
|
+
.split('\n')
|
|
996
|
+
.map(s => s.trim())
|
|
997
|
+
.filter(s => s.length);
|
|
998
|
+
btn.disabled = true;
|
|
999
|
+
statusEl.textContent = 'Saving…';
|
|
1000
|
+
try {
|
|
1001
|
+
const resp = await fetch('./api/settings/fs-custom-paths', {
|
|
1002
|
+
method: 'POST',
|
|
1003
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1004
|
+
body: JSON.stringify({ paths }),
|
|
1005
|
+
});
|
|
1006
|
+
const data = await resp.json();
|
|
1007
|
+
btn.disabled = false;
|
|
1008
|
+
if (!resp.ok || !data.success) {
|
|
1009
|
+
let msg = 'Save failed';
|
|
1010
|
+
if (data && data.error) {
|
|
1011
|
+
if (typeof data.error === 'string') msg = data.error;
|
|
1012
|
+
else if (data.error.message) msg = data.error.message;
|
|
1013
|
+
}
|
|
1014
|
+
statusEl.textContent = msg;
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
// Reflect the component's canonical normalized list; drop any rejected.
|
|
1018
|
+
_fsCustomPathsData = {
|
|
1019
|
+
...(_fsCustomPathsData || {}),
|
|
1020
|
+
available: true,
|
|
1021
|
+
paths: data.paths || [],
|
|
1022
|
+
};
|
|
1023
|
+
ta.value = (data.paths || []).join('\n');
|
|
1024
|
+
const rejected = data.rejected || [];
|
|
1025
|
+
statusEl.textContent = rejected.length
|
|
1026
|
+
? `Saved. Rejected (blocked or invalid): ${rejected.join(', ')}`
|
|
1027
|
+
: 'Saved.';
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
btn.disabled = false;
|
|
1030
|
+
statusEl.textContent = 'Network error: ' + String(err);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
891
1034
|
async function loadBackups() {
|
|
892
1035
|
const params = new URLSearchParams();
|
|
893
1036
|
const d = document.getElementById('backupDomain').value.trim();
|
|
@@ -1351,6 +1494,14 @@ function renderFeatureFlags(flags) {
|
|
|
1351
1494
|
const parentOn = !!f.value;
|
|
1352
1495
|
renderYamlPackagesSubRows(flags, targetBody, masterOn, parentOn);
|
|
1353
1496
|
}
|
|
1497
|
+
// After the enable_filesystem_tools row, inject the custom-directories
|
|
1498
|
+
// editor (issue #1567). Dimmed when either the master beta is off or
|
|
1499
|
+
// filesystem tools itself is off. The list is component-owned and fetched
|
|
1500
|
+
// separately via loadFsCustomPaths(); this renders from that cache.
|
|
1501
|
+
if (fieldName === 'enable_filesystem_tools') {
|
|
1502
|
+
const fsOn = !!f.value;
|
|
1503
|
+
renderFsCustomPathsSubForm(targetBody, masterOn, fsOn);
|
|
1504
|
+
}
|
|
1354
1505
|
});
|
|
1355
1506
|
}
|
|
1356
1507
|
|
|
@@ -2718,6 +2869,7 @@ document.getElementById('advSaveBtn').addEventListener('click', saveAdvancedSett
|
|
|
2718
2869
|
loadFeatureFlags();
|
|
2719
2870
|
loadAdvancedSettings();
|
|
2720
2871
|
loadTools();
|
|
2872
|
+
loadFsCustomPaths();
|
|
2721
2873
|
|
|
2722
2874
|
// Auto-activate tab from ?tab=<name> query string (used by approval URLs
|
|
2723
2875
|
// generated by the policy middleware: /settings?tab=tool-security-policies&token=...).
|
|
@@ -2912,6 +2912,159 @@ def build_settings_handlers(
|
|
|
2912
2912
|
}
|
|
2913
2913
|
)
|
|
2914
2914
|
|
|
2915
|
+
async def _fs_custom_paths_call(service: str, data: dict[str, Any]) -> Any:
|
|
2916
|
+
"""Invoke a ha_mcp_tools component service for the custom-paths editor,
|
|
2917
|
+
in any deployment mode (issue #1567).
|
|
2918
|
+
|
|
2919
|
+
Uses the live server's HA client in HTTP/add-on modes; in the stdio
|
|
2920
|
+
sidecar (``server is None``) builds a transient ``HomeAssistantClient``
|
|
2921
|
+
from the HA URL/token the sidecar inherits, and closes it afterward.
|
|
2922
|
+
The caller wraps this in try/except so an unreachable HA / missing
|
|
2923
|
+
token (e.g. OAuth mode, where ``server.client`` has no request-scoped
|
|
2924
|
+
token) degrades to an "unavailable" envelope rather than a 500.
|
|
2925
|
+
"""
|
|
2926
|
+
from .tools.tools_filesystem import call_mcp_tools_service
|
|
2927
|
+
|
|
2928
|
+
own_client = None
|
|
2929
|
+
try:
|
|
2930
|
+
if server is not None:
|
|
2931
|
+
client = server.client
|
|
2932
|
+
else:
|
|
2933
|
+
from .client.rest_client import HomeAssistantClient
|
|
2934
|
+
|
|
2935
|
+
client = own_client = HomeAssistantClient()
|
|
2936
|
+
return await call_mcp_tools_service(client, service, data)
|
|
2937
|
+
finally:
|
|
2938
|
+
if own_client is not None and hasattr(own_client, "close"):
|
|
2939
|
+
with contextlib.suppress(Exception):
|
|
2940
|
+
await own_client.close()
|
|
2941
|
+
|
|
2942
|
+
async def _get_fs_custom_paths(_: Request) -> JSONResponse:
|
|
2943
|
+
"""Return the user-configured extra filesystem directories from the
|
|
2944
|
+
ha_mcp_tools component, plus the non-overridable deny floor for the UI
|
|
2945
|
+
blurb (issue #1567).
|
|
2946
|
+
|
|
2947
|
+
Always 200s with an ``available`` flag: when filesystem tools are off,
|
|
2948
|
+
the component is missing/too old, or HA is unreachable, ``available``
|
|
2949
|
+
is False with a human-readable ``reason`` so the UI can show a disabled
|
|
2950
|
+
section instead of an error.
|
|
2951
|
+
"""
|
|
2952
|
+
from .tools.tools_filesystem import is_filesystem_tools_enabled
|
|
2953
|
+
from .tools.util_helpers import unwrap_service_response
|
|
2954
|
+
|
|
2955
|
+
def _unavailable(reason: str) -> JSONResponse:
|
|
2956
|
+
return JSONResponse(
|
|
2957
|
+
{
|
|
2958
|
+
"success": True,
|
|
2959
|
+
"available": False,
|
|
2960
|
+
"reason": reason,
|
|
2961
|
+
"paths": [],
|
|
2962
|
+
"deny_floor": [],
|
|
2963
|
+
}
|
|
2964
|
+
)
|
|
2965
|
+
|
|
2966
|
+
if not is_filesystem_tools_enabled():
|
|
2967
|
+
return _unavailable(
|
|
2968
|
+
"Filesystem tools are disabled. Enable them (beta) to "
|
|
2969
|
+
"configure custom directories."
|
|
2970
|
+
)
|
|
2971
|
+
try:
|
|
2972
|
+
result = await _fs_custom_paths_call("get_allowed_paths", {})
|
|
2973
|
+
except Exception as exc:
|
|
2974
|
+
logger.warning("fs-custom-paths GET could not reach ha_mcp_tools: %s", exc)
|
|
2975
|
+
return _unavailable(f"Could not reach the ha_mcp_tools component: {exc}")
|
|
2976
|
+
|
|
2977
|
+
data = unwrap_service_response(result) if isinstance(result, dict) else {}
|
|
2978
|
+
if not isinstance(data, dict) or not data.get("success", False):
|
|
2979
|
+
reason = (
|
|
2980
|
+
data.get("error") if isinstance(data, dict) else None
|
|
2981
|
+
) or "ha_mcp_tools returned an unexpected response."
|
|
2982
|
+
return _unavailable(str(reason))
|
|
2983
|
+
return JSONResponse(
|
|
2984
|
+
{
|
|
2985
|
+
"success": True,
|
|
2986
|
+
"available": True,
|
|
2987
|
+
"paths": data.get("paths", []),
|
|
2988
|
+
"deny_floor": data.get("deny_floor", []),
|
|
2989
|
+
"builtin_read_dirs": data.get("builtin_read_dirs", []),
|
|
2990
|
+
"builtin_write_dirs": data.get("builtin_write_dirs", []),
|
|
2991
|
+
}
|
|
2992
|
+
)
|
|
2993
|
+
|
|
2994
|
+
async def _save_fs_custom_paths(request: Request) -> JSONResponse:
|
|
2995
|
+
"""Replace the user-configured extra filesystem directories via the
|
|
2996
|
+
ha_mcp_tools component (issue #1567).
|
|
2997
|
+
|
|
2998
|
+
The component validates each entry and drops anything that hits the
|
|
2999
|
+
deny floor or escapes the config dir; the dropped entries come back in
|
|
3000
|
+
``rejected``. ``restart_required`` is False — the component applies the
|
|
3001
|
+
new allowlist live.
|
|
3002
|
+
"""
|
|
3003
|
+
from .tools.tools_filesystem import is_filesystem_tools_enabled
|
|
3004
|
+
from .tools.util_helpers import unwrap_service_response
|
|
3005
|
+
|
|
3006
|
+
if not is_filesystem_tools_enabled():
|
|
3007
|
+
return JSONResponse(
|
|
3008
|
+
create_error_response(
|
|
3009
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
3010
|
+
"Filesystem tools are disabled; enable them (beta) before "
|
|
3011
|
+
"configuring custom directories.",
|
|
3012
|
+
),
|
|
3013
|
+
status_code=409,
|
|
3014
|
+
)
|
|
3015
|
+
try:
|
|
3016
|
+
body = await request.json()
|
|
3017
|
+
except (ValueError, TypeError):
|
|
3018
|
+
return JSONResponse(
|
|
3019
|
+
create_error_response(
|
|
3020
|
+
ErrorCode.VALIDATION_INVALID_JSON,
|
|
3021
|
+
"Request body must be valid JSON.",
|
|
3022
|
+
),
|
|
3023
|
+
status_code=400,
|
|
3024
|
+
)
|
|
3025
|
+
paths = body.get("paths") if isinstance(body, dict) else None
|
|
3026
|
+
if paths is None:
|
|
3027
|
+
paths = []
|
|
3028
|
+
if not isinstance(paths, list) or not all(isinstance(p, str) for p in paths):
|
|
3029
|
+
return JSONResponse(
|
|
3030
|
+
create_error_response(
|
|
3031
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
3032
|
+
"'paths' must be a list of directory strings.",
|
|
3033
|
+
),
|
|
3034
|
+
status_code=400,
|
|
3035
|
+
)
|
|
3036
|
+
try:
|
|
3037
|
+
result = await _fs_custom_paths_call("set_allowed_paths", {"paths": paths})
|
|
3038
|
+
except Exception as exc:
|
|
3039
|
+
logger.warning("fs-custom-paths POST could not reach ha_mcp_tools: %s", exc)
|
|
3040
|
+
return JSONResponse(
|
|
3041
|
+
create_error_response(
|
|
3042
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
3043
|
+
f"Could not reach the ha_mcp_tools component: {exc}",
|
|
3044
|
+
),
|
|
3045
|
+
status_code=502,
|
|
3046
|
+
)
|
|
3047
|
+
|
|
3048
|
+
data = unwrap_service_response(result) if isinstance(result, dict) else {}
|
|
3049
|
+
if not isinstance(data, dict) or not data.get("success", False):
|
|
3050
|
+
reason = (
|
|
3051
|
+
data.get("error") if isinstance(data, dict) else None
|
|
3052
|
+
) or "ha_mcp_tools rejected the update."
|
|
3053
|
+
return JSONResponse(
|
|
3054
|
+
create_error_response(ErrorCode.SERVICE_CALL_FAILED, str(reason)),
|
|
3055
|
+
status_code=502,
|
|
3056
|
+
)
|
|
3057
|
+
return JSONResponse(
|
|
3058
|
+
{
|
|
3059
|
+
"success": True,
|
|
3060
|
+
"applied": data.get("paths", []),
|
|
3061
|
+
"paths": data.get("paths", []),
|
|
3062
|
+
"rejected": data.get("rejected", []),
|
|
3063
|
+
"mode": "component",
|
|
3064
|
+
"restart_required": False,
|
|
3065
|
+
}
|
|
3066
|
+
)
|
|
3067
|
+
|
|
2915
3068
|
handlers: dict[str, Any] = {
|
|
2916
3069
|
"root_page": _root_page,
|
|
2917
3070
|
"settings_page": _settings_page,
|
|
@@ -2931,6 +3084,8 @@ def build_settings_handlers(
|
|
|
2931
3084
|
"delete_backups_bulk": _delete_backups_bulk,
|
|
2932
3085
|
"get_backup_config": _get_backup_config,
|
|
2933
3086
|
"save_backup_config": _save_backup_config,
|
|
3087
|
+
"get_fs_custom_paths": _get_fs_custom_paths,
|
|
3088
|
+
"save_fs_custom_paths": _save_fs_custom_paths,
|
|
2934
3089
|
}
|
|
2935
3090
|
|
|
2936
3091
|
# Tool security policies. The main server attaches an
|
|
@@ -3100,6 +3255,9 @@ def register_settings_routes(
|
|
|
3100
3255
|
("/api/settings/backups/{name}", ["DELETE"], "delete_backup"),
|
|
3101
3256
|
("/api/settings/backup-config", ["GET"], "get_backup_config"),
|
|
3102
3257
|
("/api/settings/backup-config", ["POST"], "save_backup_config"),
|
|
3258
|
+
# Custom filesystem directories (issue #1567) — component-owned list
|
|
3259
|
+
("/api/settings/fs-custom-paths", ["GET"], "get_fs_custom_paths"),
|
|
3260
|
+
("/api/settings/fs-custom-paths", ["POST"], "save_fs_custom_paths"),
|
|
3103
3261
|
# Tool security policies endpoints
|
|
3104
3262
|
("/api/policy/config", ["GET"], "policy_get_config"),
|
|
3105
3263
|
("/api/policy/config", ["PUT"], "policy_put_config"),
|
|
@@ -602,6 +602,21 @@ def _build_app(
|
|
|
602
602
|
handlers["save_feature_flags"],
|
|
603
603
|
methods=["POST"],
|
|
604
604
|
),
|
|
605
|
+
# Custom filesystem directories (issue #1567). The sub-form is rendered
|
|
606
|
+
# in the features panel, so the stdio sidecar must serve these too — its
|
|
607
|
+
# route list is hand-maintained and does NOT derive from
|
|
608
|
+
# register_settings_routes. The handler builds a transient HA client
|
|
609
|
+
# from the inherited env when server is None (sidecar mode).
|
|
610
|
+
Route(
|
|
611
|
+
f"{secret_prefix}/api/settings/fs-custom-paths",
|
|
612
|
+
handlers["get_fs_custom_paths"],
|
|
613
|
+
methods=["GET"],
|
|
614
|
+
),
|
|
615
|
+
Route(
|
|
616
|
+
f"{secret_prefix}/api/settings/fs-custom-paths",
|
|
617
|
+
handlers["save_fs_custom_paths"],
|
|
618
|
+
methods=["POST"],
|
|
619
|
+
),
|
|
605
620
|
# Tool security policies endpoints (#966). Pending/approve/deny
|
|
606
621
|
# are wired as stubs that return 503 in sidecar mode — the
|
|
607
622
|
# in-memory ApprovalQueue lives in the main server process, so
|
{ha_mcp_dev-7.6.0.dev675 → ha_mcp_dev-7.6.0.dev677}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
@@ -197,14 +197,24 @@ def check_automation_config(
|
|
|
197
197
|
|
|
198
198
|
warnings = BestPracticeCheckResult()
|
|
199
199
|
|
|
200
|
+
# Read the canonical 2024.10+ plural root keys, falling back to the singular
|
|
201
|
+
# aliases. The internal pipeline always pre-normalizes to plural, so the
|
|
202
|
+
# fallback is defensive for direct/public callers of check_automation_config
|
|
203
|
+
# (HA accepts both forms). Mirrors _check_triggers' platform/trigger tolerance.
|
|
200
204
|
# Condition templates
|
|
201
|
-
_check_condition_templates(
|
|
205
|
+
_check_condition_templates(
|
|
206
|
+
config.get("conditions", config.get("condition", [])), warnings, skill_prefix
|
|
207
|
+
)
|
|
202
208
|
|
|
203
209
|
# Action tree (wait_template + nested conditions + target templates)
|
|
204
|
-
_check_action_tree(
|
|
210
|
+
_check_action_tree(
|
|
211
|
+
config.get("actions", config.get("action", [])), warnings, skill_prefix
|
|
212
|
+
)
|
|
205
213
|
|
|
206
214
|
# Trigger templates + device_id
|
|
207
|
-
_check_triggers(
|
|
215
|
+
_check_triggers(
|
|
216
|
+
config.get("triggers", config.get("trigger", [])), warnings, skill_prefix
|
|
217
|
+
)
|
|
208
218
|
|
|
209
219
|
# Mode vs motion pattern
|
|
210
220
|
_check_mode_motion(config, warnings, skill_prefix)
|
|
@@ -738,7 +748,7 @@ def _check_mode_motion(
|
|
|
738
748
|
if mode != "single":
|
|
739
749
|
return
|
|
740
750
|
|
|
741
|
-
triggers = _as_list(config.get("trigger", []))
|
|
751
|
+
triggers = _as_list(config.get("triggers", config.get("trigger", [])))
|
|
742
752
|
has_motion = any(
|
|
743
753
|
isinstance(t, dict)
|
|
744
754
|
and any(
|
|
@@ -750,7 +760,7 @@ def _check_mode_motion(
|
|
|
750
760
|
if not has_motion:
|
|
751
761
|
return
|
|
752
762
|
|
|
753
|
-
if _has_delay_or_wait(config.get("action", [])):
|
|
763
|
+
if _has_delay_or_wait(config.get("actions", config.get("action", []))):
|
|
754
764
|
_emit(
|
|
755
765
|
warnings,
|
|
756
766
|
"Automation uses motion trigger with delay/wait but "
|