ha-mcp-dev 8.3.0.dev2419__tar.gz → 8.3.0.dev2425__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-8.3.0.dev2419/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.3.0.dev2425}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/pyproject.toml +3 -2
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/__main__.py +2 -69
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/client/rest_client.py +42 -40
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/client/supervisor_client.py +13 -22
- ha_mcp_dev-8.3.0.dev2425/src/ha_mcp/log_filters.py +207 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_addons.py +824 -452
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/setup.cfg +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/renamed_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/bulk_selector.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/component_config_entries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/config_entry_identity.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/entity_membership.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "8.3.0.
|
|
7
|
+
version = "8.3.0.dev2425"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# Upper bound widened to <3.15 so the package installs on Home Assistant's
|
|
@@ -256,8 +256,9 @@ markers = [
|
|
|
256
256
|
"error_handling: error handling and edge case tests",
|
|
257
257
|
"cleanup: tests that create entities needing cleanup",
|
|
258
258
|
"performance: performance measurement and regression tests",
|
|
259
|
-
"external_only:
|
|
259
|
+
"external_only: in-process-server tiers only; skips inaddon, stdio, container-embedded, and HAOS-embedded servers",
|
|
260
260
|
"inaddon_only: only runs on HAOS inaddon tier",
|
|
261
|
+
"beta_haos_only: beta HAOS image lanes with Supervisor/Core version expectations configured",
|
|
261
262
|
]
|
|
262
263
|
asyncio_mode = "auto"
|
|
263
264
|
|
|
@@ -33,15 +33,13 @@ import threading # noqa: E402
|
|
|
33
33
|
from collections.abc import Coroutine # noqa: E402
|
|
34
34
|
from typing import TYPE_CHECKING, Any, NoReturn # noqa: E402
|
|
35
35
|
|
|
36
|
-
from fastmcp.exceptions import ToolError # noqa: E402
|
|
37
|
-
from pydantic import ValidationError as PydanticValidationError # noqa: E402
|
|
38
|
-
|
|
39
36
|
from ha_mcp.browser_landing import ( # noqa: E402
|
|
40
37
|
register_browser_landing as _register_landing_route,
|
|
41
38
|
)
|
|
42
39
|
from ha_mcp.browser_landing import ( # noqa: E402
|
|
43
40
|
register_healthz as _register_healthz_route,
|
|
44
41
|
)
|
|
42
|
+
from ha_mcp.log_filters import install_sdk_log_filters # noqa: E402
|
|
45
43
|
|
|
46
44
|
if TYPE_CHECKING:
|
|
47
45
|
from fastmcp import FastMCP
|
|
@@ -384,68 +382,6 @@ mcp = _DeferredMCP()
|
|
|
384
382
|
_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
385
383
|
|
|
386
384
|
|
|
387
|
-
class StatelessSessionLogFilter(logging.Filter):
|
|
388
|
-
"""Suppress the routine 'Terminating session: None' log from the MCP SDK.
|
|
389
|
-
|
|
390
|
-
In stateless HTTP mode every request creates and tears down a temporary
|
|
391
|
-
session whose id is ``None``, so the SDK emits an INFO
|
|
392
|
-
``Terminating session: None`` (mcp/server/streamable_http.py) on *every*
|
|
393
|
-
request. The line is routine but looks alarming and has repeatedly
|
|
394
|
-
confused users into thinking the connection is broken.
|
|
395
|
-
|
|
396
|
-
Returning ``False`` drops the record at this logger before it reaches any
|
|
397
|
-
handler. (Merely downgrading the level to DEBUG did not work: the level
|
|
398
|
-
gate is applied before the filter runs, so the record was already admitted
|
|
399
|
-
and still emitted -- just relabelled.) Real session terminations carry an
|
|
400
|
-
actual id and are not matched, so they still log.
|
|
401
|
-
|
|
402
|
-
# TODO: remove when modelcontextprotocol/python-sdk#2329 is resolved
|
|
403
|
-
"""
|
|
404
|
-
|
|
405
|
-
def filter(self, record: logging.LogRecord) -> bool:
|
|
406
|
-
if record.name != "mcp.server.streamable_http":
|
|
407
|
-
return True
|
|
408
|
-
try:
|
|
409
|
-
message = record.getMessage()
|
|
410
|
-
except (ValueError, TypeError):
|
|
411
|
-
# A malformed %-format record on this logger is not our target, and
|
|
412
|
-
# a filter must not raise: filters run in Logger.handle() with no
|
|
413
|
-
# exception handling, so a raise would crash the logging call.
|
|
414
|
-
return True
|
|
415
|
-
# Drop the stateless teardown noise; keep everything else.
|
|
416
|
-
return "Terminating session: None" not in message
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
class ToolValidationLogFilter(logging.Filter):
|
|
420
|
-
"""Demote fastmcp tool-failure tracebacks to single-line warnings.
|
|
421
|
-
|
|
422
|
-
Pydantic ValidationError and tool-raised ToolError aren't server bugs,
|
|
423
|
-
so the traceback through fastmcp/pydantic internals is just noise. The
|
|
424
|
-
structured error detail is preserved in the WARNING message; stack is
|
|
425
|
-
intentionally dropped because these are user-input errors, not bugs.
|
|
426
|
-
"""
|
|
427
|
-
|
|
428
|
-
def filter(self, record: logging.LogRecord) -> bool:
|
|
429
|
-
if record.name != "fastmcp.server.server" or not record.exc_info:
|
|
430
|
-
return True
|
|
431
|
-
|
|
432
|
-
msg = record.getMessage()
|
|
433
|
-
err = record.exc_info[1]
|
|
434
|
-
if "Error validating tool" in msg and isinstance(err, PydanticValidationError):
|
|
435
|
-
record.msg = f"{msg}: {err.errors(include_url=False)}"
|
|
436
|
-
elif "Error calling tool" in msg and isinstance(err, ToolError):
|
|
437
|
-
record.msg = f"{msg}: {err}"
|
|
438
|
-
else:
|
|
439
|
-
return True
|
|
440
|
-
|
|
441
|
-
record.args = ()
|
|
442
|
-
record.levelno = logging.WARNING
|
|
443
|
-
record.levelname = "WARNING"
|
|
444
|
-
record.exc_info = None
|
|
445
|
-
record.exc_text = None
|
|
446
|
-
return True
|
|
447
|
-
|
|
448
|
-
|
|
449
385
|
class ProbeAccessLogFilter(logging.Filter):
|
|
450
386
|
"""Drop benign, non-MCP HTTP probe noise from the uvicorn access log.
|
|
451
387
|
|
|
@@ -528,10 +464,7 @@ def _setup_logging(log_level_str: str, force: bool = True) -> None:
|
|
|
528
464
|
# disabled. Prevent that NOTSET namespace from inheriting our root handler.
|
|
529
465
|
fastmcp_logger.setLevel(logging.CRITICAL + 1)
|
|
530
466
|
|
|
531
|
-
|
|
532
|
-
StatelessSessionLogFilter()
|
|
533
|
-
)
|
|
534
|
-
logging.getLogger("fastmcp.server.server").addFilter(ToolValidationLogFilter())
|
|
467
|
+
install_sdk_log_filters()
|
|
535
468
|
|
|
536
469
|
|
|
537
470
|
def _log_startup_version() -> None:
|
|
@@ -704,8 +704,9 @@ class HomeAssistantClient:
|
|
|
704
704
|
async def get_addon_logs(self, slug: str, lines: int | None = None) -> str:
|
|
705
705
|
"""Fetch an add-on's container logs.
|
|
706
706
|
|
|
707
|
-
Branch on ``is_running_in_addon()
|
|
708
|
-
|
|
707
|
+
Branch on ``is_running_in_addon()``, which requires a truthy
|
|
708
|
+
``SUPERVISOR_TOKEN`` and excludes ``HA_MCP_EMBEDDED``: inside the app
|
|
709
|
+
(add-on) container goes directly to the Supervisor
|
|
709
710
|
REST API at ``http://supervisor/addons/{slug}/logs`` with the
|
|
710
711
|
Supervisor token. The HA Core proxy at
|
|
711
712
|
``/api/hassio/addons/{slug}/logs`` rejects this token+path combination
|
|
@@ -730,8 +731,9 @@ class HomeAssistantClient:
|
|
|
730
731
|
Raises:
|
|
731
732
|
HomeAssistantAuthError: 401 response, or ``SUPERVISOR_TOKEN`` empty
|
|
732
733
|
at call time on the addon branch.
|
|
733
|
-
HomeAssistantAPIError: 403 (
|
|
734
|
-
``
|
|
734
|
+
HomeAssistantAPIError: 403 (unrecognized token, missing
|
|
735
|
+
``hassio_api``, or insufficient ``hassio_role``), 404
|
|
736
|
+
(unknown slug), or other non-2xx. The
|
|
735
737
|
``status_code`` attribute lets callers map to specific
|
|
736
738
|
suggestions.
|
|
737
739
|
HomeAssistantConnectionError: Network, timeout, or transport error.
|
|
@@ -775,25 +777,28 @@ class HomeAssistantClient:
|
|
|
775
777
|
|
|
776
778
|
``path`` is everything between ``http://supervisor/`` and ``/logs``:
|
|
777
779
|
|
|
778
|
-
- ``"addons/<slug>"`` for add-on container logs
|
|
780
|
+
- ``"addons/<slug>"`` for app (add-on) container logs
|
|
779
781
|
- ``"<service>"`` (where service ∈ {supervisor, host, core, dns, audio,
|
|
780
782
|
cli, multicast, observer}) for system-service logs
|
|
781
783
|
|
|
782
784
|
``lines`` maps to the endpoint's ``?lines=`` journald-window query
|
|
783
785
|
param; omitted → Supervisor's 100-line default window.
|
|
784
786
|
|
|
785
|
-
Bypasses ``HomeAssistantClient.httpx_client`` because
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
787
|
+
Bypasses ``HomeAssistantClient.httpx_client`` because that client targets
|
|
788
|
+
Home Assistant Core through ``http://supervisor/core/api``, while logs
|
|
789
|
+
belong to Supervisor at ``http://supervisor``. Both clients use the same
|
|
790
|
+
``SUPERVISOR_TOKEN``; the Core proxy requires ``homeassistant_api``, while
|
|
791
|
+
system-service and arbitrary app-log paths require ``hassio_api`` and
|
|
792
|
+
``hassio_role: manager``. The recognized-app-token exception is
|
|
793
|
+
``/addons/self/logs``.
|
|
790
794
|
|
|
791
795
|
Raises:
|
|
792
796
|
HomeAssistantAuthError: ``SUPERVISOR_TOKEN`` absent at call time,
|
|
793
797
|
or 401 from Supervisor.
|
|
794
|
-
HomeAssistantAPIError: 403 (
|
|
795
|
-
role
|
|
796
|
-
``{"result":"error","message":"..."}``
|
|
798
|
+
HomeAssistantAPIError: 403 (unrecognized token, missing
|
|
799
|
+
``hassio_api``, or insufficient role), 404, other 4xx/5xx.
|
|
800
|
+
Parses Supervisor's ``{"result":"error","message":"..."}``
|
|
801
|
+
JSON envelope before
|
|
797
802
|
falling back to text body / reason phrase / placeholder.
|
|
798
803
|
HomeAssistantConnectionError: Timeout or transport error, with
|
|
799
804
|
distinct messages so callers can tell them apart.
|
|
@@ -839,18 +844,19 @@ class HomeAssistantClient:
|
|
|
839
844
|
if response.status_code == 401:
|
|
840
845
|
raise HomeAssistantAuthError(f"Invalid Supervisor token for /{path}/logs")
|
|
841
846
|
if response.status_code == 403:
|
|
842
|
-
#
|
|
843
|
-
#
|
|
844
|
-
#
|
|
845
|
-
#
|
|
847
|
+
# Supervisor uses 403 for an unrecognized app token, missing
|
|
848
|
+
# hassio_api permission, or a hassio_role that cannot access this
|
|
849
|
+
# endpoint. The default-to-manager role bump fixed #1116, but it is
|
|
850
|
+
# not the only possible cause.
|
|
846
851
|
logger.warning(
|
|
847
|
-
"Supervisor returned 403 for /%s/logs —
|
|
848
|
-
"
|
|
852
|
+
"Supervisor returned 403 for /%s/logs — check token, hassio_api, "
|
|
853
|
+
"and hassio_role (need 'manager')",
|
|
849
854
|
path,
|
|
850
855
|
)
|
|
851
856
|
raise HomeAssistantAPIError(
|
|
852
|
-
f"Supervisor forbids /{path}/logs (403) —
|
|
853
|
-
"may
|
|
857
|
+
f"Supervisor forbids /{path}/logs (403) — token may be unrecognized, "
|
|
858
|
+
"app may lack hassio_api, or hassio_role may not allow this endpoint "
|
|
859
|
+
"(manager required)",
|
|
854
860
|
status_code=403,
|
|
855
861
|
response_data={"path": path},
|
|
856
862
|
)
|
|
@@ -873,14 +879,15 @@ class HomeAssistantClient:
|
|
|
873
879
|
async def _get_addon_logs_via_supervisor(
|
|
874
880
|
self, slug: str, lines: int | None = None
|
|
875
881
|
) -> str:
|
|
876
|
-
"""Fetch add-on container logs directly from Supervisor
|
|
882
|
+
"""Fetch app (add-on) container logs directly from Supervisor REST.
|
|
877
883
|
|
|
878
|
-
Distinct from ``tools_bug_report._fetch_addon_logs``: that
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
+
Distinct from ``tools_bug_report._fetch_addon_logs``: that auxiliary
|
|
885
|
+
helper uses the app self-service path ``/addons/self/logs`` and may skip
|
|
886
|
+
failures. This helper takes arbitrary slugs and surfaces failures because
|
|
887
|
+
callers (``ha_get_logs(source="supervisor", slug=...)``) need them.
|
|
888
|
+
Arbitrary app-log slugs require a recognized app token with ``hassio_api``
|
|
889
|
+
and ``hassio_role: manager``; ``/addons/self/logs`` bypasses the role and
|
|
890
|
+
API-permission checks but still requires a recognized app token.
|
|
884
891
|
|
|
885
892
|
Delegates to ``_supervisor_logs_get`` so error handling stays in
|
|
886
893
|
lockstep with ``_get_system_service_logs``.
|
|
@@ -895,26 +902,21 @@ class HomeAssistantClient:
|
|
|
895
902
|
``service`` must be one of the eight Supervisor-managed services:
|
|
896
903
|
``supervisor``, ``host``, ``core``, ``dns``, ``audio``, ``cli``,
|
|
897
904
|
``multicast``, ``observer``. Caller is responsible for validating
|
|
898
|
-
``service
|
|
899
|
-
|
|
905
|
+
``service``; this helper performs no validation, and unsupported paths
|
|
906
|
+
are rejected by the selected direct-Supervisor or Core-proxy route.
|
|
900
907
|
|
|
901
908
|
Branch on ``is_running_in_addon()`` — mirror of ``get_addon_logs``:
|
|
902
|
-
inside the
|
|
909
|
+
inside the app (add-on) container goes directly to Supervisor at
|
|
903
910
|
``http://supervisor/{service}/logs`` with the Supervisor token
|
|
904
|
-
(``hassio_role: manager`` required). On non-
|
|
911
|
+
(``hassio_api`` and ``hassio_role: manager`` required). On non-app
|
|
912
|
+
installs (Docker
|
|
905
913
|
without Supervisor, pyinstaller, pip pointing at a normal HA URL),
|
|
906
914
|
falls back to the HA Core proxy at ``/api/hassio/{service}/logs``.
|
|
907
915
|
|
|
908
|
-
All
|
|
916
|
+
All eight service slugs are whitelisted in HA Core's hassio proxy
|
|
909
917
|
(``homeassistant/components/hassio/http.py`` — ``PATHS_ADMIN``), so
|
|
910
918
|
an admin LLA is sufficient to reach any of them from outside the
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
Closes #1260: pre-fix this method had only the addon-direct branch,
|
|
914
|
-
so non-addon installs (the Docker image, uvx ha-mcp, etc.) hit the
|
|
915
|
-
``SUPERVISOR_TOKEN``-absent fail-fast in ``_supervisor_logs_get`` for
|
|
916
|
-
every service, while the sibling ``source="supervisor"`` (addon
|
|
917
|
-
logs) call kept working through its own Core-proxy fallback.
|
|
919
|
+
app.
|
|
918
920
|
"""
|
|
919
921
|
if is_running_in_addon():
|
|
920
922
|
return await self._supervisor_logs_get(service, lines=lines)
|
{ha_mcp_dev-8.3.0.dev2419 → ha_mcp_dev-8.3.0.dev2425}/src/ha_mcp/client/supervisor_client.py
RENAMED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
"""Shared factory for direct-Supervisor httpx clients.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
``
|
|
6
|
-
|
|
3
|
+
Several code paths call the Home Assistant Supervisor REST API directly with
|
|
4
|
+
a short-lived client configured for the Supervisor base URL and
|
|
5
|
+
``SUPERVISOR_TOKEN``. Callers cover log collection, app (add-on) management
|
|
6
|
+
and restart, backup operations, and screenshot-engine discovery.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- :func:`ha_mcp.settings_ui._handlers_server._restart_addon` — POSTs
|
|
13
|
-
``/addons/self/restart`` from the settings UI
|
|
14
|
-
|
|
15
|
-
All three share the same boilerplate (base URL, ``Authorization: Bearer
|
|
16
|
-
${SUPERVISOR_TOKEN}`` header), so this module supplies a single factory and
|
|
17
|
-
keeps the three sites consistent.
|
|
8
|
+
These calls cannot use ``HomeAssistantClient.httpx_client``, which targets
|
|
9
|
+
Home Assistant Core rather than Supervisor. In app mode it carries the same
|
|
10
|
+
token value, but requests use a different base URL and authorization surface.
|
|
11
|
+
This module centralizes the direct transport setup.
|
|
18
12
|
"""
|
|
19
13
|
|
|
20
14
|
from __future__ import annotations
|
|
@@ -56,14 +50,11 @@ def make_supervisor_httpx_client(
|
|
|
56
50
|
|
|
57
51
|
Raises:
|
|
58
52
|
RuntimeError: ``SUPERVISOR_TOKEN`` is unset or empty in the
|
|
59
|
-
environment.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
``Authorization: Bearer `` header from being read as a token
|
|
65
|
-
rejection by Supervisor, which would mask the missing-env-var
|
|
66
|
-
root cause.
|
|
53
|
+
environment. Callers must either gate construction on token
|
|
54
|
+
presence or translate this exception for their response surface.
|
|
55
|
+
Detecting it here prevents a malformed
|
|
56
|
+
``Authorization: Bearer `` header from masking the missing
|
|
57
|
+
environment variable as a token rejection from Supervisor.
|
|
67
58
|
|
|
68
59
|
Note:
|
|
69
60
|
``SUPERVISOR_TOKEN`` is read from env at construction time and
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""MCP SDK / fastmcp log-noise filters shared across every HTTP launcher.
|
|
2
|
+
|
|
3
|
+
Extracted from :mod:`ha_mcp.__main__` for the same reason as
|
|
4
|
+
:mod:`ha_mcp.browser_landing`: the in-process server (the ``ha_mcp_tools``
|
|
5
|
+
custom-component worker thread) must never import ``ha_mcp.__main__``, since
|
|
6
|
+
that module runs process-global side effects at import time
|
|
7
|
+
(``truststore.inject_into_ssl()``, signal handlers, ``asyncio.run``). These
|
|
8
|
+
filters only call ``logging.Logger.addFilter`` on specific named loggers --
|
|
9
|
+
no ``basicConfig``, no handler or root-logger changes -- so
|
|
10
|
+
``install_sdk_log_filters()`` is safe to call from any launcher, including
|
|
11
|
+
one that must leave Home Assistant's own logging configuration untouched
|
|
12
|
+
(see the ``log_config=None`` comment in ``embedded_server.py``).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
import anyio
|
|
20
|
+
from fastmcp.exceptions import ToolError
|
|
21
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StatelessSessionLogFilter(logging.Filter):
|
|
25
|
+
"""Suppress the routine 'Terminating session: None' log from the MCP SDK.
|
|
26
|
+
|
|
27
|
+
In stateless HTTP mode every request creates and tears down a temporary
|
|
28
|
+
session whose id is ``None``, so the SDK emits an INFO
|
|
29
|
+
``Terminating session: None`` (mcp/server/streamable_http.py) on *every*
|
|
30
|
+
request. The line is routine but looks alarming and has repeatedly
|
|
31
|
+
confused users into thinking the connection is broken.
|
|
32
|
+
|
|
33
|
+
Returning ``False`` drops the record at this logger before it reaches any
|
|
34
|
+
handler. (Merely downgrading the level to DEBUG did not work: the level
|
|
35
|
+
gate is applied before the filter runs, so the record was already admitted
|
|
36
|
+
and still emitted -- just relabelled.) Real session terminations carry an
|
|
37
|
+
actual id and are not matched, so they still log.
|
|
38
|
+
|
|
39
|
+
# TODO: remove when modelcontextprotocol/python-sdk#2329 is resolved
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
43
|
+
"""Drop the routine stateless-teardown record; pass everything else."""
|
|
44
|
+
if record.name != "mcp.server.streamable_http":
|
|
45
|
+
return True
|
|
46
|
+
try:
|
|
47
|
+
message = record.getMessage()
|
|
48
|
+
except (ValueError, TypeError):
|
|
49
|
+
# A malformed %-format record on this logger is not our target, and
|
|
50
|
+
# a filter must not raise: filters run in Logger.handle() with no
|
|
51
|
+
# exception handling, so a raise would crash the logging call.
|
|
52
|
+
return True
|
|
53
|
+
# Drop the stateless teardown noise; keep everything else.
|
|
54
|
+
return "Terminating session: None" not in message
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ToolValidationLogFilter(logging.Filter):
|
|
58
|
+
"""Demote fastmcp tool-failure tracebacks to single-line warnings.
|
|
59
|
+
|
|
60
|
+
Pydantic ValidationError and tool-raised ToolError aren't server bugs,
|
|
61
|
+
so the traceback through fastmcp/pydantic internals is just noise. The
|
|
62
|
+
structured error detail is preserved in the WARNING message; stack is
|
|
63
|
+
intentionally dropped because these are user-input errors, not bugs.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
67
|
+
"""Demote a known-benign validation/tool-error record to WARNING."""
|
|
68
|
+
if record.name != "fastmcp.server.server" or not record.exc_info:
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
msg = record.getMessage()
|
|
72
|
+
err = record.exc_info[1]
|
|
73
|
+
if "Error validating tool" in msg and isinstance(err, PydanticValidationError):
|
|
74
|
+
record.msg = f"{msg}: {err.errors(include_url=False)}"
|
|
75
|
+
elif "Error calling tool" in msg and isinstance(err, ToolError):
|
|
76
|
+
record.msg = f"{msg}: {err}"
|
|
77
|
+
else:
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
record.args = ()
|
|
81
|
+
record.levelno = logging.WARNING
|
|
82
|
+
record.levelname = "WARNING"
|
|
83
|
+
record.exc_info = None
|
|
84
|
+
record.exc_text = None
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_DISCONNECT_TEARDOWN_ERRORS = (anyio.ClosedResourceError, anyio.BrokenResourceError)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _is_only_disconnect_teardown_errors(err: BaseException) -> bool:
|
|
92
|
+
"""True if ``err`` is (or an ExceptionGroup wrapping only) a benign
|
|
93
|
+
streamable-HTTP memory-stream teardown error.
|
|
94
|
+
|
|
95
|
+
``mcp.server.lowlevel.server.Server.run()`` dispatches each incoming
|
|
96
|
+
message via ``anyio.create_task_group().start_soon(...)``, so an error
|
|
97
|
+
raised while delivering one message's response is raised from a
|
|
98
|
+
task-group child -- anyio always wraps that in an ``ExceptionGroup``,
|
|
99
|
+
even for a single failure. The exception a "session crashed" log record
|
|
100
|
+
carries is therefore ``ExceptionGroup(...[<teardown error>])``, not a
|
|
101
|
+
bare one.
|
|
102
|
+
|
|
103
|
+
Both ``ClosedResourceError`` and ``BrokenResourceError`` -- sibling
|
|
104
|
+
``Exception`` subclasses, neither a subclass of the other -- can surface
|
|
105
|
+
here for the same disconnect race: which one depends on which end
|
|
106
|
+
closed first (our end closing raises ``Closed``; the peer's
|
|
107
|
+
``streamable_http`` ``terminate()`` closing first can wake a parked
|
|
108
|
+
sender into raising ``Broken`` instead, even though the situation is
|
|
109
|
+
identical from the caller's perspective). This repo already treats the
|
|
110
|
+
two as an equivalent pair for streamable-HTTP memory-stream teardown
|
|
111
|
+
elsewhere -- see ``tests/src/haos_runtime.py``'s transient-error tuple.
|
|
112
|
+
|
|
113
|
+
Recurse through (possibly nested) groups; any other leaf type means
|
|
114
|
+
this is not the known-benign disconnect race, so the caller should
|
|
115
|
+
leave the record alone.
|
|
116
|
+
"""
|
|
117
|
+
if isinstance(err, _DISCONNECT_TEARDOWN_ERRORS):
|
|
118
|
+
return True
|
|
119
|
+
if isinstance(err, BaseExceptionGroup):
|
|
120
|
+
return bool(err.exceptions) and all(
|
|
121
|
+
_is_only_disconnect_teardown_errors(sub) for sub in err.exceptions
|
|
122
|
+
)
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class SessionDisconnectLogFilter(logging.Filter):
|
|
127
|
+
"""Demote 'session crashed' tracebacks caused by an already-gone client.
|
|
128
|
+
|
|
129
|
+
Every HTTP entry point runs Streamable HTTP in stateless mode (see
|
|
130
|
+
``ha_mcp.__main__._http_run_kwargs``). A tool call slow enough to outlast
|
|
131
|
+
the client's patience -- a busy Home Assistant instance, a
|
|
132
|
+
resource-contended local LLM host on the client side, or an ordinary HTTP
|
|
133
|
+
timeout -- lets the SDK finish serving the response and tear the
|
|
134
|
+
transport down while ``app.run()`` is still working; the eventual attempt
|
|
135
|
+
to deliver the response then writes into an already-closed memory stream
|
|
136
|
+
and raises ``anyio.ClosedResourceError`` or ``anyio.BrokenResourceError``
|
|
137
|
+
(wrapped in an ``ExceptionGroup`` -- see
|
|
138
|
+
``_is_only_disconnect_teardown_errors``). The SDK already catches this
|
|
139
|
+
(``except Exception: logger.exception(...)`` in both the stateless and
|
|
140
|
+
stateful session runners of mcp/server/streamable_http_manager.py) -- it
|
|
141
|
+
just logs it as an alarming ERROR-level traceback. That's an expected
|
|
142
|
+
race in a stateless HTTP protocol (the client already gave up), not a
|
|
143
|
+
server bug, so demote it the same way ToolValidationLogFilter demotes
|
|
144
|
+
other known-benign failures. Any other exception on this logger -- an
|
|
145
|
+
actual crash -- is left untouched.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
149
|
+
"""Demote a disconnect-caused 'session crashed' record to WARNING."""
|
|
150
|
+
if record.name != "mcp.server.streamable_http_manager" or not record.exc_info:
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
err = record.exc_info[1]
|
|
154
|
+
if err is None or not _is_only_disconnect_teardown_errors(err):
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
record.msg = (
|
|
158
|
+
f"{record.getMessage()}: client disconnected before response delivery"
|
|
159
|
+
)
|
|
160
|
+
record.args = ()
|
|
161
|
+
record.levelno = logging.WARNING
|
|
162
|
+
record.levelname = "WARNING"
|
|
163
|
+
record.exc_info = None
|
|
164
|
+
record.exc_text = None
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _add_filter_once(logger_name: str, filter_cls: type[logging.Filter]) -> None:
|
|
169
|
+
"""Attach one ``filter_cls`` instance to ``logger_name``, replacing any stale one.
|
|
170
|
+
|
|
171
|
+
``install_sdk_log_filters()`` can run more than once per process: the
|
|
172
|
+
in-process embedded server calls it on every ``_serve()`` reload without
|
|
173
|
+
a process restart, and process-wide ``logging`` state (including each
|
|
174
|
+
named logger's filter list) persists across those reloads. Each reload
|
|
175
|
+
also re-imports this module from scratch (the embedded server purges
|
|
176
|
+
``ha_mcp.*`` from ``sys.modules`` before reinstalling -- see
|
|
177
|
+
``_purge_ha_mcp_modules``), so ``filter_cls`` is a BRAND NEW class object
|
|
178
|
+
each time, even though its name is unchanged. A same-``isinstance``-check
|
|
179
|
+
against a filter instance from a PREVIOUS generation would therefore
|
|
180
|
+
never match, letting filters accumulate one more stale instance per
|
|
181
|
+
reload forever. Match by ``(module, qualname)`` instead -- stable across
|
|
182
|
+
reloads of the same module path -- and drop every stale-generation match
|
|
183
|
+
before attaching the current one, so a logger never carries more than
|
|
184
|
+
one filter of a given conceptual type, and it's always this generation's.
|
|
185
|
+
"""
|
|
186
|
+
logger = logging.getLogger(logger_name)
|
|
187
|
+
identity = (filter_cls.__module__, filter_cls.__qualname__)
|
|
188
|
+
logger.filters[:] = [
|
|
189
|
+
f
|
|
190
|
+
for f in logger.filters
|
|
191
|
+
if (type(f).__module__, type(f).__qualname__) != identity
|
|
192
|
+
]
|
|
193
|
+
logger.addFilter(filter_cls())
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def install_sdk_log_filters() -> None:
|
|
197
|
+
"""Attach the demotion filters above to their target SDK/fastmcp loggers.
|
|
198
|
+
|
|
199
|
+
Every HTTP launcher must call this: the CLI (``ha_mcp.__main__``), the
|
|
200
|
+
Home Assistant app's ``start.py``, and the in-process embedded server
|
|
201
|
+
(``ha_mcp_tools/embedded_server.py``) each build and run their own
|
|
202
|
+
Streamable HTTP app, so none of them share another launcher's logging
|
|
203
|
+
setup. Safe to call repeatedly -- see ``_add_filter_once``.
|
|
204
|
+
"""
|
|
205
|
+
_add_filter_once("mcp.server.streamable_http", StatelessSessionLogFilter)
|
|
206
|
+
_add_filter_once("mcp.server.streamable_http_manager", SessionDisconnectLogFilter)
|
|
207
|
+
_add_filter_once("fastmcp.server.server", ToolValidationLogFilter)
|