"""Constants for the HA-MCP custom component. The integration serves two config-entry types under one domain (:data:`DOMAIN`), discriminated by ``entry.data[CONF_ENTRY_TYPE]``: * ``tools`` — the privileged file / YAML services (the original component). Pre-existing entries carry no ``entry_type`` key, so a missing value is treated as ``tools`` (no migration needed). * ``server`` — the in-process ha-mcp FastMCP server (issue #1527), exposed through a Home Assistant webhook. The two halves keep their constants in separate blocks below; the ``server`` block was folded in from the former standalone ``ha_mcp_server`` integration. """ import re from datetime import timedelta DOMAIN = "ha_mcp_tools" # Component version, kept in lockstep with ``manifest.json``'s ``version``. # ``ha_mcp_tools/info`` reports this so the server can display/debug the running # component build; ``TestManifestVersionParity`` pins the two together so a # manifest bump that forgets this constant (or vice-versa) fails in CI. The # capability negotiation — not this version — gates each WS command (see # ``websocket_api.CAPABILITIES``). COMPONENT_VERSION = "1.2.3" # Config-entry discriminator (``entry.data[CONF_ENTRY_TYPE]``). A missing value # means "tools" so the pre-existing services entry keeps working across the # component update with no migration. CONF_ENTRY_TYPE = "entry_type" ENTRY_TYPE_TOOLS = "tools" ENTRY_TYPE_SERVER = "server" # Titles shown for each entry in the integration tile's entry list. Public so # __init__'s setup migration can retitle pre-#1853 tools entries still # carrying the legacy default (a user-customized title is left alone). TOOLS_ENTRY_TITLE = "HA-MCP File & YAML Tools" TOOLS_ENTRY_LEGACY_TITLE = "HA MCP Tools" MIN_EMBEDDED_HOME_ASSISTANT_VERSION = "2026.6.0" # Allowed directories for file operations (relative to config dir). # "blueprints" is read-only BY DEFAULT — in ALLOWED_READ_DIRS but not # ALLOWED_WRITE_DIRS, so ha_write_file / ha_delete_file reject it (raw blueprint # reads are safe: community YAML, no secrets — issue #1965). This is the default # allowlist, not an absolute guarantee: an admin who adds "blueprints" as a # custom extra directory (issue #1567, see _current_extra_dirs) grants it # read+write, since extra_dirs are honored on the write path too. Blueprint # writes should instead go through ha_import_blueprint (which invokes the # blueprint/save WS command internally). Prefer ha_get_blueprint for the parsed # body; raw read is the escape hatch for the exact on-disk text. ALLOWED_READ_DIRS = ["www", "themes", "custom_templates", "dashboards", "blueprints"] ALLOWED_WRITE_DIRS = ["www", "themes", "custom_templates", "dashboards"] # NON-OVERRIDABLE deny floor for the user-configurable extra read/write # directories (issue #1567). The custom allowlist is applied ON TOP of the # built-in ALLOWED_*_DIRS, but a custom directory can NEVER grant access to # these. The floor is re-checked before any allow decision on every read, # write, list, and delete, so neither a stored entry nor an in-flight one can # punch through it. # # .storage holds HA's auth database (refresh/access tokens), hashed passwords, # and every integration's cleartext credentials (core.config_entries, # application_credentials, cloud) — including this component's OWN caller # token (.storage/ha_mcp_tools_auth). Letting a custom dir reach it would both # leak secrets and hand out the key to this component's own auth gate. DENY_PATH_SEGMENTS = frozenset({".storage"}) # secrets.yaml is reachable ONLY as the canonical config-root file, where the # read handler masks its values. Any OTHER secrets.yaml surfaced via a custom # dir would be returned UNMASKED (masking keys off the literal root path), so # the floor blocks the basename everywhere except that one canonical location. DENY_READ_BASENAMES = frozenset({"secrets.yaml"}) # HAOS sibling-volume mounts (issue #1586). These live OUTSIDE the config dir, # so the config-relative custom-directory allowlist (issue #1567) cannot reach # them — its normalizer rejects every absolute path. A user may instead add one # of these fixed absolute roots — or a subdirectory of one — to the custom # directory list; access is then enforced against the volume root exactly as a # config-relative entry is enforced against the config dir (issue #1586). # # The component runs inside HA Core, so a volume is reachable only if the HA # Core container actually mounts it (the standard HAOS/Supervised mounts are # config/share/media/ssl/backup). An unmounted or non-existent root simply # yields a "not found" at use time — adding it is harmless. As with the # config-relative list, a configured volume grants BOTH read and write, and the # non-overridable deny floor (.storage / secrets.yaml) still applies. ALLOWED_VOLUME_ROOTS = ("/share", "/media", "/ssl", "/backup") # Files allowed for managed YAML editing ALLOWED_YAML_CONFIG_FILES = ["configuration.yaml"] # Also allows /*.yaml via pattern matching, where the folder is # the one the user binds under ``homeassistant: packages:`` (default "packages", # detected at runtime — see _detect_package_dirs), plus themes/*.yaml. # Top-level YAML keys allowed for editing in any allowed file # (configuration.yaml or packages/*.yaml). # ONLY keys that have no UI/API alternative belong here. # Keys manageable via ha_config_set_helper (input_*, counter, timer, schedule) # are intentionally excluded. automation/script/scene live in # PACKAGES_ONLY_YAML_KEYS below — they have storage-mode equivalents # (ha_config_set_automation/script/scene) but are still exposed in # packages/*.yaml for the YAML-packages workflow. ALLOWED_YAML_KEYS = frozenset( { "template", "sensor", "binary_sensor", "command_line", "rest", "knx", "mqtt", "shell_command", "switch", "light", "fan", "cover", "climate", "notify", "group", "utility_meter", # recorder is YAML-only (no UI or storage-mode helper): purge_keep_days, # include/exclude, commit_interval. Its surface is smaller than keys # already here — it only controls what HA records and for how long, with # no code-execution path like command_line/shell_command/rest (#1852). "recorder", } ) # Top-level YAML keys allowed ONLY inside packages/*.yaml files, never in # configuration.yaml. Storage-mode UI/API equivalents already exist # (ha_config_set_automation/script/scene), so these are exposed here only # for the YAML-packages workflow used by git-managed configs — where users # expect to keep automations/scripts/scenes alongside templates and other # YAML-defined items. Writes to configuration.yaml for these keys remain # rejected so storage-mode and YAML-mode collections don't collide. PACKAGES_ONLY_YAML_KEYS = frozenset( { "automation", "script", "scene", } ) # Post-edit action required for each YAML key. # template, mqtt, group, automation, script, and scene have first-party # reload services in HA core. All others require a full HA restart. # ``TestPostActionTableContract`` pins the in-repo shape; the HA-core # side of the contract is a write-time snapshot, not a continuous check. YAML_KEY_POST_ACTIONS: dict[str, dict[str, str]] = { "template": { "post_action": "reload_available", "reload_service": "homeassistant.reload_custom_templates", }, "mqtt": { "post_action": "reload_available", "reload_service": "mqtt.reload", }, "group": { "post_action": "reload_available", "reload_service": "group.reload", }, "automation": { "post_action": "reload_available", "reload_service": "automation.reload", }, "script": { "post_action": "reload_available", "reload_service": "script.reload", }, "scene": { "post_action": "reload_available", "reload_service": "scene.reload", }, } # Default for keys not in YAML_KEY_POST_ACTIONS: YAML_KEY_DEFAULT_POST_ACTION = {"post_action": "restart_required"} # YAML-mode dashboard url_path validation (issue #1034). # Pattern: lowercase letters/digits, hyphen-separated, must contain at least # one hyphen (HA's lovelace dashboard rule). No leading/trailing/double hyphens. DASHBOARD_URL_PATH_PATTERN = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)+") # url_paths reserved by HA core dashboards/routes — must not be registered as # YAML-mode dashboards or they will shadow / collide with built-ins. RESERVED_DASHBOARD_URL_PATHS = frozenset( { "lovelace", "overview", "map", "logbook", "history", "energy", "developer-tools", "config", "profile", "media-browser", "todo", "calendar", } ) # --------------------------------------------------------------------------- # HA-MCP Server entry (issue #1527) # # Folded in from the former standalone ``ha_mcp_server`` integration. The # "server" config-entry type runs the full ha-mcp FastMCP server in-process # inside Home Assistant (a dedicated thread with its own asyncio loop) and # exposes it remotely through a Home Assistant webhook, exactly like the # webhook-proxy add-on. Creating the entry starts the server; disabling or # removing the entry stops it. Everything below is namespaced under the shared # ``DOMAIN`` (distinct hass.data sub-keys, distinct entry unique_id). # --------------------------------------------------------------------------- # PyPI distribution names. Stable ships as ``ha-mcp``; the dev channel ships as # ``ha-mcp-dev`` — published on every master push. BOTH are installed unpinned, # so every install / reload resolves the newest build of the selected channel # (the component auto-updates the server rather than pinning a lockstep version # — see ``UPDATE_CHECK_INTERVAL`` and # ``EmbeddedServerManager._async_ensure_package``). Both wheels contain the # *same* ``ha_mcp`` import package (publish-dev.yml only renames the # distribution), so only one may be installed at a time — see # EmbeddedServerManager's channel-switch handling. DIST_NAME_STABLE = "ha-mcp" DIST_NAME_DEV = "ha-mcp-dev" # Default pip requirement for the stable channel: the unpinned ``ha-mcp`` # distribution, so each install resolves the newest stable release. The options # flow's advanced "pip requirement" field overrides this with any pip spec # (e.g. a version pin or a GitHub tarball URL) for pre-release testing — an # explicit override also disables automatic updates. DEFAULT_PIP_SPEC = DIST_NAME_STABLE DEV_PIP_SPEC = DIST_NAME_DEV # Release channels (options-flow selector). ``stable`` installs the unpinned # ``ha-mcp`` and ``dev`` installs the unpinned ``ha-mcp-dev``; both refresh to # the newest build of that channel on every entry reload / HA restart, and the # periodic auto-update check reloads the entry when PyPI publishes a newer one. # An explicit OPT_PIP_SPEC override wins over both and disables auto-update. CHANNEL_STABLE = "stable" CHANNEL_DEV = "dev" DEFAULT_CHANNEL = CHANNEL_STABLE def dist_for_channel(channel: str) -> str: """Map a release channel to its PyPI distribution name. The channel <-> distribution correspondence is used by the version coordinator, the auto-update notification, and the server manager's pip resolution — one shared mapping so a future third channel cannot be added to some sites and missed in others (review finding on #1760). """ return DIST_NAME_DEV if channel == CHANNEL_DEV else DIST_NAME_STABLE def channel_for_dist(dist: str) -> str: """Inverse of :func:`dist_for_channel`.""" return CHANNEL_DEV if dist == DIST_NAME_DEV else CHANNEL_STABLE # Interval of the ServerVersionCoordinator's PyPI poll (coordinator.py). The # poll itself ALWAYS runs — it feeds the `update` platform entity, which must # stay populated even when automatic updates are off (issue #1760). Whether a # newer build actually triggers a reload/reinstall is decided separately, per # refresh, in embedded_setup.async_maybe_auto_update (gated on OPT_AUTO_UPDATE # and on no pip-spec override). Only an explicit pip-spec override skips the # PyPI fetch — comparing PyPI-latest against an arbitrary pip spec is # meaningless. UPDATE_CHECK_INTERVAL = timedelta(hours=6) # PyPI JSON API for the latest published version of a distribution. ``{dist}`` # is DIST_NAME_STABLE or DIST_NAME_DEV depending on the selected channel. PYPI_JSON_URL = "https://pypi.org/pypi/{dist}/json" # The component manifest as it existed at a server release's git tag. Its # ``version`` is the component version that SHIPPED with that server build, so # a value newer than the running component means the release changed the # component too — the pre-install auto-update gate in embedded_setup holds the # server update until HACS delivers the component (issues #1783/#1785). # Tag-timing caveat: stable ``vX.Y.Z`` tags exist before the PyPI publish # (semantic-release pushes the tag first), but a dev ``vX.Y.Z.devN`` tag is # only created when its draft GitHub release is published — AFTER the binary # builds, minutes after PyPI already has the version. During that dev window # this URL 404s and the gate deliberately fails open (the registry's # skip-on-failure is the backstop on that channel). COMPONENT_MANIFEST_AT_TAG_URL = ( "https://raw.githubusercontent.com/homeassistant-ai/ha-mcp/" "v{version}/custom_components/ha_mcp_tools/manifest.json" ) # Options-flow keys (stored in entry.options). OPT_CHANNEL = "channel" # Automatic server-version updates toggle (default on). When on, the channel is # unpinned and auto-updates (force-install on reload/restart + a reload when the # periodic check sees a newer build). When off, the server stays on the version # currently installed: _resolve_pip_spec pins the channel's dist to that version # — but the periodic PyPI check KEEPS running so the update entity still shows # newer builds; its Install button is the manual path (issue #1760). Governs the # ha-mcp server package only — component updates still come through HACS. An # explicit OPT_PIP_SPEC override wins over both and skips the check entirely. OPT_AUTO_UPDATE = "auto_update" DEFAULT_AUTO_UPDATE = True OPT_SERVER_PORT = "server_port" OPT_BIND_HOST = "bind_host" OPT_WEBHOOK_AUTH = "webhook_auth" # Legacy OAuth mode (self-hosted authorization server, static client_id/secret # for Google Gemini Spark) credential management — mirrors the # OPT_WEBHOOK_ID_OVERRIDE / OPT_REGENERATE_SECRETS shape below. Empty override # fields mean "keep the current value"; OPT_OAUTH_REGENERATE is one-shot. # _override suffix distinguishes these OPTIONS keys from the DATA_OAUTH_* # entry.data keys (which store the resolved values under the un-suffixed # names) — mirrors OPT_WEBHOOK_ID_OVERRIDE vs DATA_WEBHOOK_ID. OPT_OAUTH_CLIENT_ID = "oauth_client_id_override" OPT_OAUTH_CLIENT_SECRET = "oauth_client_secret_override" OPT_OAUTH_REGENERATE = "oauth_regenerate" OPT_PIP_SPEC = "pip_spec" OPT_SERVER_URL = "server_url" # Connect-URL surface + secret management (owner request, parity with the # webhook-proxy app's external-URL option and the add-on's secret-path # override). All optional; empty string = automatic/keep-current. OPT_EXTERNAL_URL = "external_url" OPT_WEBHOOK_ID_OVERRIDE = "webhook_id_override" OPT_SECRET_PATH_OVERRIDE = "secret_path_override" OPT_REGENERATE_SECRETS = "regenerate_secrets" # Local-only mode (owner request): when False, the HA webhook is never # registered, so nothing - including Nabu Casa remote UI - can reach the # server through Home Assistant; only the direct server port (+ the # admin-only sidebar panel, which proxies over loopback) remains. OPT_ENABLE_WEBHOOK = "enable_webhook" # Conversation-agent LLM API (#1745): when False, the toolset is not # registered as a Home Assistant LLM API, so it never appears in any # conversation agent's "Control Home Assistant" selector. On by default — # registering the API only makes it selectable; nothing is exposed until a # user picks it on an agent. OPT_ENABLE_LLM_API = "enable_llm_api" DEFAULT_ENABLE_LLM_API = True # Which exposure shape(s) the LLM API offers to conversation agents: # ``tool_search`` (default) registers a compact API — pinned tools plus # search/execute meta-tools — the shape context-limited models need; ``full`` # registers the whole exposed catalog as one API; ``both`` registers the two # side by side so the choice is made per agent in HA's own selector. OPT_LLM_API_EXPOSURE = "llm_api_exposure" EXPOSURE_TOOL_SEARCH = "tool_search" EXPOSURE_FULL = "full" EXPOSURE_BOTH = "both" DEFAULT_LLM_API_EXPOSURE = EXPOSURE_TOOL_SEARCH # When False, the persistent notification created on every server bring-up is # suppressed; the connect URLs still reach the admin-only Home Assistant log. OPT_ENABLE_STARTUP_NOTIFICATION = "enable_startup_notification" # When False, the admin-only "HA-MCP" sidebar settings panel is not registered; # the server's options stay reachable on the entry's Configure screen. OPT_ENABLE_SIDEBAR_PANEL = "enable_sidebar_panel" # entry.data keys (persisted ids + secrets; entry.data is fine for secrets). DATA_WEBHOOK_ID = "webhook_id" DATA_SECRET_PATH = "secret_path" # Legacy OAuth mode credentials, minted by embedded_entry._ensure_secrets and # consumed by oauth_legacy.LegacyOAuthProvider. DATA_OAUTH_SIGNING_KEY is a hex # string (entry.data must be JSON-serializable, so raw bytes aren't stored # directly) — the provider converts it with bytes.fromhex(). The signed token # payload carries the client_id (not the secret), so rotating the client_id # revokes every outstanding token at the restart that rebinds the views (see # LegacyOAuthProvider._validate_token). # Because validation never involves the client_secret, a secret-only override # change instead rotates the signing key, evicting outstanding tokens at the # restart that activates the new credentials (see # embedded_entry._ensure_legacy_oauth_secrets). Until that restart the bound # views keep serving the OLD identity, so the startup log withholds rotated # credentials (embedded_setup._surface_connect_urls). DATA_OAUTH_CLIENT_ID = "oauth_client_id" DATA_OAUTH_CLIENT_SECRET = "oauth_client_secret" DATA_OAUTH_SIGNING_KEY = "oauth_signing_key" DATA_SERVER_USER_ID = "server_user_id" DATA_REFRESH_TOKEN_ID = "refresh_token_id" DATA_ACCESS_TOKEN = "access_token" # Last pip spec that was successfully installed. Lets a changed spec (the # pre-release test channel) force an actual reinstall on the next start instead # of hitting the requirements manager's is-installed shortcut. DATA_LAST_PIP_SPEC = "last_pip_spec" # One-shot marker set by the update entity's Install button (issue #1760): # with auto-update off, EmbeddedServerManager._resolve_pip_spec pins the # channel to the CURRENTLY installed version, so a bare reload would just # reinstall the same build. This pins the next install to a specific version # regardless of auto_update; embedded_server clears it when it CONSUMES it # (before the install attempt) — one marker buys exactly one attempt, so a # failing pinned version can never re-pin later reloads (review finding). DATA_PENDING_INSTALL_VERSION = "pending_install_version" # hass.data[DOMAIN] sub-keys for the server runtime. Distinct from the tools # entry's sub-keys ("caller_token" / "allowed_paths") so both entry types can # share hass.data[DOMAIN] without collision. DATA_MANAGER = "manager" DATA_WEBHOOK = "webhook" DATA_BRINGUP_TASK = "bringup_task" # Snapshot of entry.options taken at setup so the update listener reloads only # on a genuine options change — the background bring-up persists ids/token/pip # spec to entry.data, and those writes must not trigger a self-reload. DATA_LAST_OPTIONS = "last_options" # The ServerVersionCoordinator instance backing the `update` platform entity # (issue #1760) — stored so the platform's async_setup_entry can retrieve it. DATA_UPDATE_COORDINATOR = "update_coordinator" # Set by async_maybe_auto_update right before it reloads the entry for an # automatic update ({"old": }): the "server updated" notification must # only fire once the reloaded entry's bring-up actually installed and started # the new build — the reload call returns as soon as entry SETUP finishes, # while the pip install still runs in the background and can fail (review # finding on #1760). Bring-up pops it: notification on success, silent drop on # failure (the package/start repair issues cover that path). DATA_PENDING_UPDATE_NOTIFY = "pending_update_notify" # Unregister callback for the conversation-agent LLM API (#1745), stored by # the bring-up success path and invoked (idempotently) by teardown. DATA_LLM_API_UNSUB = "llm_api_unsub" # Webhook auth modes (mirrors the webhook-proxy add-on's default posture). WEBHOOK_AUTH_NONE = "none" # secret webhook URL is the shared secret (default) WEBHOOK_AUTH_HA = "ha_auth" # HA-native bearer (HA core is the OAuth AS) # Self-hosted OAuth 2.1 authorization server with a static client_id/secret, # ported from the webhook-proxy add-on's "legacy" mode. Needed because HA # core's /auth/authorize does not yet fetch Client ID Metadata Documents for # cross-origin redirect_uris (home-assistant/core#176282), which is what # Google Gemini Spark's custom connected apps require. WEBHOOK_AUTH_LEGACY = "legacy" # Default bind host + port. 9584 (not the add-on's 9583) so this in-process # server and an add-on install can coexist on the same box. DEFAULT_SERVER_PORT = 9584 # LAN-reachable by default - parity with the add-on, whose port has always # been directly reachable with the secret path as the credential. Loopback # is the optional hardening choice, not the default (owner decision). DEFAULT_BIND_HOST = "0.0.0.0" BIND_HOST_ALL = "0.0.0.0" BIND_HOST_LOOPBACK = "127.0.0.1" # Loopback base URL the server uses to reach HA core (REST + WS). DEFAULT_LOOPBACK_URL = "http://127.0.0.1:8123" # Persistent data dir for the in-process server, under the HA config dir so it # survives restarts and is isolated from an add-on's /data. Generic ".ha_mcp" # to match the merged integration's naming (unreleased server entry, so no # migration from the former ".ha_mcp_server"). SERVER_CONFIG_SUBDIR = ".ha_mcp" # Client name recorded on the provisioned long-lived access token, and the name # of the local admin user the server logs in as. Stable so a reused token is # recognizable in Settings -> People -> -> tokens. "HA-MCP" phrasing (not # "Home Assistant MCP Server") to avoid confusion with HA's official MCP Server # integration. SERVER_TOKEN_CLIENT_NAME = "HA-MCP Server" SERVER_USER_NAME = "HA-MCP Server" # RFC 8414 / RFC 9728 discovery documents for ha_auth mode are served under this # namespace (mirrors the webhook-proxy add-on's /api/mcp_proxy/oauth base). OAUTH_BASE = "/api/ha_mcp_tools/oauth" # HACS repository full_names (``owner/repo``, the key HACS's repository registry # uses) this component may be tracked under: the dedicated integration mirror is # the current install path; the main ha-mcp server repo is the legacy pre-mirror # path (see install_source_check). Shared by the legacy-source check and the # HACS refresh nudge (hacs_nudge) so a repository rename lands in one place. HACS_MIRROR_REPO_FULL_NAME = "homeassistant-ai/ha-mcp-integration" HACS_LEGACY_REPO_FULL_NAME = "homeassistant-ai/ha-mcp" # HACS "add repository" deep link for the custom component. learn_more_url for # the legacy-HACS-source repair (install_source_check) only, whose fix really is # re-adding the repository. The update-held and component-outdated issues point # at UPDATE_HOLD_DOCS_URL instead — for an already-installed component this deep # link just opens a blank "add repository" dialog. HACS_COMPONENT_URL = ( "https://my.home-assistant.io/redirect/hacs_repository/" "?owner=homeassistant-ai&repository=ha-mcp-integration&category=integration" ) # Docs section explaining the automatic-update hold, linked as learn_more_url # from the update-held and component-outdated repair issues (both resolve by # updating an already-installed component, not by re-adding a repository). The # anchor is the GitHub slug of the "Held server updates" heading in # docs/in-process-server.md; hassfest forbids literal URLs inside strings.json. UPDATE_HOLD_DOCS_URL = ( "https://github.com/homeassistant-ai/ha-mcp/blob/master/docs/" "in-process-server.md#held-server-updates" ) # Usage guide for the conversation-agent LLM API option (#1745). Injected into # the options form as a description placeholder — hassfest forbids literal # URLs inside strings.json. LLM_API_DOCS_URL = ( "https://github.com/homeassistant-ai/ha-mcp/blob/master/docs/" "in-process-server.md" "#chat-with-the-toolset-from-home-assistant-conversation-agents--voice" ) # Repair-issue ids surfaced when server bring-up fails. ISSUE_PACKAGE_FAILED = "server_package_install_failed" ISSUE_START_FAILED = "server_start_failed" # Repair issue surfaced when the installed ha-mcp server requires a newer # custom component than the one running. The server package updates # independently of the HACS component, so the running component can lag what # the server expects; this points the user at the HACS component update # (non-blocking). ISSUE_COMPONENT_OUTDATED = "component_outdated" # Repair issue surfaced while an automatic server update is HELD because the # newer server release also shipped a newer custom component than the one # running (issues #1783/#1785): installing that server under the old component # is the combination that broke starts. Held is loud (this issue + a warning # log every check) and escapable — applying the HACS component update (which # takes an HA restart) unblocks the next check, and the update entity's # Install button bypasses the hold entirely. ISSUE_UPDATE_HELD = "server_update_held" # Repair issue surfaced when HACS is tracking the MAIN ha-mcp server repo for # this component (the pre-mirror install path — issue #1760). That install # keeps working (HACS downloads the repo snapshot at the release tag, which # contains the component), but HACS shows the SERVER's version numbers and # release notes, not the component's own; HACS has no repository-migration # mechanism, so this only self-resolves if the user re-adds the dedicated # mirror (homeassistant-ai/ha-mcp-integration). ISSUE_LEGACY_HACS_SOURCE = "legacy_hacs_source" # Repair issue surfaced when the legacy OAuth mode's root /authorize + /token # views are out of sync with the CONFIGURED webhook_auth mode — either just # enabled (views not yet bound with the current credentials) or just disabled # (views still bound and serving the old identity). aiohttp can neither bind # nor unbind an HTTP view without a full Home Assistant restart, so both # transitions need one; see oauth_legacy.bind_legacy_views. ISSUE_LEGACY_OAUTH_RESTART = "legacy_oauth_restart"