A thin add-on that does NOT run an MCP server. It discovers a running ha-mcp server
add-on (stable ha_mcp, then dev ha_mcp_dev), installs a custom component into HA
Core, registers a webhook (/api/webhook/<id>) plus optional OAuth 2.1 views, and
proxies remote MCP traffic to the server add-on's local port. Built locally by
Supervisor from the Dockerfile (no prebuilt image) — a "release" is a version bump in
config.yaml on the branch the add-on store points at.
- Stable: dir
homeassistant-addon-webhook-proxy/, slugha_mcp_webhook_proxy, component domainmcp_proxy,boot: auto. - Dev: dir
homeassistant-addon-webhook-proxy-dev/, slugha_mcp_webhook_proxy_dev, component domainmcp_proxy_dev,boot: manual,stage: experimental.
They are a hand-maintained duplicate — no codegen. The dev tree is the stable tree
with every mcp_proxy token rewritten to mcp_proxy_dev (this also renames the
/config/.mcp_proxy_* state files, /opt/mcp_proxy, the /api/mcp_proxy/oauth base,
the HA config-entry domain, and /config/custom_components/mcp_proxy). /data/* files
(webhook id, OAuth creds) are already isolated per add-on. CI test
tests/src/unit/test_webhook_proxy_dev_isolation.py fails if any bare mcp_proxy token
leaks into the dev tree.
A PR never edits the stable tree directly in regular operation. The
webhook-proxy-stable-guard workflow blocks any PR touching
homeassistant-addon-webhook-proxy/ unless it comes from a promote-webhook-proxy/*
branch or carries the allow-stable-edit label for an exceptional direct repair. Every change
(code and docs) lands on the dev flavor first — with the version bump
webhook-proxy-dev-version-guard enforces — gets tested on the dev channel, and
reaches stable through the promote workflow (see Promotion below).
The only exception is this file (and its CLAUDE.md symlink): it is the contributor
doc for both flavors (the dev tree carries only a pointer stub to it), the promote
transform never touches it, and the guard exempts it — edit it directly in a normal
PR. The exemption is pinned to exactly these two paths in their doc shape (regular
file + symlink to it); anything else at or under those names stays guarded. DOCS.md
is part of the transform (token rename + flavor-banner swap) and follows the dev-first
flow like code; CHANGELOG.md stays per-flavor and gets a manual entry on each
promote PR.
Both flavors install a webhook + OAuth views; the OAuth provider owns the root
/authorize and /token routes, which two live integrations cannot share. (The scoped
/api/mcp_proxy{,_dev}/oauth/* routes carry the domain, so those do not collide — the
bare root pair is what forces the exclusion.) So only one
flavor may run at a time. Each start.py checks the Supervisor /addons list on
startup and refuses (logs + a self-clearing HA notification, then exits) if its sibling
is started. start.py:_sibling_is_running matches the sibling by exact slug or
_<base> suffix (Supervisor hash-prefixes third-party slugs).
- Dev: the version is BASED ON THE CURRENT STABLE version — the next stable
patch with a
.devNsuffix (stable2.0.2→2.0.3.dev1,2.0.3.dev2, …), so dev always sorts ahead of the stable it will promote into. Bumphomeassistant-addon-webhook-proxy-dev/config.yamlversionANDmcp_proxy_dev/manifest.jsonversiontogether (they must stay equal). Thewebhook-proxy-dev-version-guardworkflow fails any PR that touches the dev add-on without an increase. Use theWebhook Proxy Dev — Bump Versionworkflow (workflow_dispatch, never scheduled) to do the bump and open a draft PR, or edit the two files by hand. (Dev entries belowv2.0.3.dev1in the dev CHANGELOG predate this rule — the line originally counted independently from the 1.2.2 fork point.) - Stable keeps its own version line and never inherits a
.devNlabel; the promote workflow assigns the stable number.
When the dev flavor is ready to become stable, run the Webhook Proxy — Promote Dev to Stable workflow (workflow_dispatch): it runs scripts/webhook_proxy_sync.py --direction promote, verifies the result with the drift guards
(tests/src/unit/test_webhook_proxy_sync.py), bumps stable's own version, and opens a
draft promote PR. The transform also carries DOCS.md across (component-token
rename plus the flavor-banner swap; the canonical banners live in DOCS_BANNERS
in the sync script, kept honest by test_docs_banners_match_canonical).
AGENTS.md and CHANGELOG.md are left untouched — review them by hand on that
PR (test_stable_docs_free_of_dev_identity backstops the banner swap so dev
framing can never ship to stable users). What the transform does (also the
manual fallback):
- Copy the changed dev files onto the stable dir.
- Reverse-rename
mcp_proxy_dev->mcp_proxyeverywhere (the inverse of the dev transform): component dirmcp_proxy_dev/->mcp_proxy/,DOMAIN,/optpath,/config/.mcp_proxy_dev_*state files,/api/mcp_proxy_dev/oauthbase, config-entry domain, and the add-on slugha_mcp_webhook_proxy_dev->ha_mcp_webhook_proxy. - In
start.py, the stable mutual-exclusion constants areSIBLING_SLUG_BASE = "ha_mcp_webhook_proxy_dev",MUTEX_NOTIFICATION_ID = "mcp_proxy_mutex",SIBLING_LABEL = "Webhook Proxy (Dev)"(do not copy the dev-side values). - Set
boot: auto, dropstage: experimental, remove the(Dev)display-name suffixes. - Bump stable's own
config.yaml+manifest.jsonversion (its independent line). - Rebase the dev flavor's version line onto the new stable in the SAME promote PR
(next stable patch +
.dev1, per the Versioning rule above — the transform'srebase_dev_versiondoes this; only devconfig.yaml+manifest.jsonchange). Right after a promote the trees are code-identical, so no separate reset PR is needed;Reset Dev from Stableremains a manual tool after an exceptional direct stable repair.
tests/addon/test_webhook_proxy.py is parametrized over BOTH flavors — an autouse
_webhook_proxy_variant fixture rebinds PROXY_ADDON_DIR/CURRENT, so every test runs
once as [stable] and once as [dev]. CI runs tests/addon/, so the dev code is
exercised on every PR — that is what makes it safe to develop on the dev flavor before
promoting. When you add a variant-specific VALUE — a path, slug, domain, or route base —
add it to the WEBHOOK_PROXY_VARIANTS table rather than hard-coding it in a test. A
variant-specific CAPABILITY is the opposite: feature-detect it from the flavor's own
source, the way _ha_auth_supported, _none_autoapprove_supported, and
_rfc9207_iss_supported already do. The promote transform copies code but never edits
this test file, so a capability recorded as a table boolean stays False for stable
after the code arrives, and the promote PR then asserts the old surface against the new
one. tests/src/unit/test_webhook_proxy_dev_isolation.py
separately guards the rename (no bare mcp_proxy token in the dev tree) and the dev-side
mutual-exclusion constants.