[APP][Pro] MCP AI Bridge - Control Homey with any AI assistant (Claude, ChatGPT, Gemini, Copilot or Cursor) via the Model Context Protocol

@LionelDerBoven
Your AI was exactly right, and that’s a genuinely clean catch. :bullseye: It’s a real bug in MCP AI Bridge, not in your app.

flowcards_test_condition was returning Homey’s response envelope ({ result: <boolean>, error, usedTokens, elapsedTime }) and coercing the whole object to a boolean with !!. An object is always truthy, so the tool reported true for every condition regardless of the real answer, which is why a 0% random, a non-matching day, and your everyone_asleep card all came back true while the raw data and CLI correctly said false.

Fixed in 2.8.1: it now reads the actual .result boolean. Verified live: chance=0 → false, chance=100 → true. I also hardened flowcards_run_action so it reports a failure when a card returns an error instead of always claiming success.

The chance:0 reproduction was about as airtight a bug report as it gets, thanks for that, and for confirming your own app was behaving perfectly the whole time. Update to 2.8.1 once it’s live.

Also checked every other run/test/trigger tool for the same mistake, they’re clean: flow triggers throw on error (so they can’t silently report success), the simulate tool is validation-only, and the rest either read the right field or fail loudly. flowcards_test_condition was the only one affected, fixed in 2.8.1.

Thank you! This is fixed indeed! I stumbled on on small issue when making my app (your app is helping so mutch with this with the MCP access)!

I let Claude make a raport of the bug.

Bug report — MCP AI Bridge serves stale device capability values

App: MCP AI Bridge 2.8.1 (app store, live channel) — Danny van Soerland
Homey: Homey Pro 2023 (platform local v2), firmware 13.3.0
Client: Claude Code via HTTP MCP transport (http://<homey-ip>:52199/mcp)
Date: 2026-07-28


Summary

The device tools return capability values from a cache that appears to be
populated once and then never refreshed. The cache does not update when a
capability changes, and is not invalidated even by the bridge’s own
devices_set_capability write.

The stale response includes a stale lastUpdated timestamp, so it is a whole
cached device object being returned, not just a stale value on a fresh object.

This is a silent failure: the response is well-formed and plausible, so a client
has no way to tell it is being given old data. It cost me a debugging session
chasing a bug in my own app that did not exist.

Affected (confirmed): devices_get_state, devices_find
Not affected (confirmed accurate): users_list, users_get_presence,
apps_get, apps_list, flows_list, flows_get, system_get_memory,
flowcards_test_condition


Reproduction

The device is a simple onoff device provided by a local app, whose value is
changed by that app calling setCapabilityValue(). Ground truth is read with the
Homey CLI, which bypasses the bridge entirely:

homey api devices get-device --id <device-id> --json \
  --jq '"onoff=\(.capabilitiesObj.onoff.value) lastUpdated=\(.capabilitiesObj.onoff.lastUpdated)"'

Timeline

Clock (UTC) Event
07:37:42.258 App writes onoff = true. CLI confirms, lastUpdated matches.
07:40:31.650 App writes onoff = false. CLI confirms.
07:42:27 CLI: onoff=false lastUpdated=07:40:31.650Z
MCP devices_get_state: value=true lastUpdated=07:37:42.258Z
07:42:46.491 App writes onoff = true. CLI confirms.
07:43:11 CLI: onoff=true lastUpdated=07:42:46.491Z
MCP: value=true lastUpdated=07:37:42.258Z — value now correct by coincidence, timestamp still frozen
07:43:32.759 App writes onoff = false. CLI confirms.
07:43:50 CLI: onoff=false lastUpdated=07:43:32.759Z
MCP devices_get_state: onoff=true
MCP devices_find (capability onoff, operator truthy): returns this device with value=true
07:44 (after) Called MCP devices_set_capabilityonoff=false, returned success:true.
Immediately re-read via MCP devices_get_state: still value=true lastUpdated=07:37:42.258Z

Throughout, every MCP response reported the identical timestamp
2026-07-28T07:37:42.258Z, while Homey’s own value changed four times.


Key observations

  1. The cache never refreshes. More than six minutes and four capability
    changes later, the bridge still returned the value from 07:37:42.
  2. lastUpdated is stale too. A client cannot detect the staleness by
    comparing timestamps, because the cached timestamp is returned as well.
  3. The bridge’s own writes do not invalidate it. devices_set_capability
    reports success, but a subsequent devices_get_state still returns the old
    cached object.
  4. The cache is shared across device tools. devices_get_state and
    devices_find both returned the same stale value in the same instant, so it
    looks like a shared device-state cache rather than a per-tool issue.
  5. It is domain-specific. Users, apps, flows, flow cards and system memory
    were all accurate at the same moments. Only device capability state was stale.

Possible cause (guess, not verified)

The timestamp the cache is frozen at (07:37:42) is the moment a local app that
had been running under homey app run --remote wrote a capability value. Shortly
after, that dev session ended and the app was reinstalled with
homey app install, restarting it.

That makes me suspect the bridge maintains its device cache from a realtime
subscription which was dropped, or silently stopped delivering, around an app
restart — and is never re-established or re-synced afterwards. Worth checking
whether the subscription is re-created on reconnect, and whether there is any
TTL or periodic re-sync as a backstop.


Impact

Any automation or assistant reading device state through the bridge can act on
arbitrarily old data while believing it is current. Because the payload is
well-formed and carries a plausible timestamp, there is no client-side signal
that anything is wrong — the failure mode is silent and indistinguishable from
correct behaviour.


Suggested fixes

  • Re-establish or re-sync the device subscription on reconnect, and after an app
    restart is observed.
  • Invalidate the cached entry on devices_set_capability, at minimum.
  • Add a TTL so a broken subscription degrades to “slightly stale” rather than
    “frozen indefinitely”.
  • Consider passing through Homey’s lastUpdated only when it comes from a fresh
    read, so clients can detect staleness themselves.

Note on a previously reported issue

flowcards_test_condition used to always return {"result": true} regardless of
the card’s real value. That was reported and fixed — re-verified today:
homey:manager:logic:random with chance: 0 now correctly returns false.
Thank you for the quick turnaround.

The technique that caught it is worth reusing as a regression test: call a card
whose answer is knowable a priori. logic:random with chance: 0 must always be
false; cron:day_equals with a day that is not today must always be false.

@LionelDerBoven
Confirmed and fixed in 2.8.2 , your analysis was exactly right, including the root cause. The bridge reads devices through homey-api’s realtime item-store; the EventBridge puts the devices manager into realtime mode, and when that socket silently drops (as it did around your homey app runinstall restart), the store freezes and every read returns the value + timestamp from that moment, with no re-sync. Device reads now pass $cache:false so they always do a fresh HTTP read regardless of socket state (verified: an external capability change is now reflected immediately with a fresh lastUpdated). I also found and fixed the same silent-drop failure on the event/webhook path (a watchdog poll now keeps it alive). Your chance:0 regression-test idea was spot on too, and while auditing this I fixed a batch of other issues (an XSS in dashboard config, a couple of access-scope classifications, OAuth refresh rotation, and several list fields). Thank you, genuinely one of the best bug reports I’ve had.

Confirmed and fixed. Thank you! And no problem. Thank Claude Opus 5 for the detailed markdown bugreport :smiley:

:tada: Version 3.0 is here, the biggest update yet!

After a long deep-audit build run, MCP AI Bridge 3.0 is out. This one packs the most new features of any release so far, with security and polish across the board.

Highlights:

  • :brain: Smarter answers — trend and anomaly detection on your Insights, so “is my power use climbing this week?” gets a real analysis.
  • :eye: The AI can see now ,camera snapshots come back as actual images, not just links.
  • :shield: Guardrails with a visual editor ,click together rules like “never unlock doors at night”, with a strict mode that also blocks the sneaky paths through scenes, flows and scripts.
  • :white_check_mark: Confirmation on the scary stuff ,reboot, delete and uninstall ask a capable client to confirm first.
  • :key: Tighter keys ,per-key rate limits, last-used tracking, and remote connectors you can scope to read / write / full per connection.
  • :high_voltage: Live-push dashboards ,the dashboard now updates the instant something changes through the bridge, no more waiting for a refresh.
  • :compass: Tidy Advanced Flows ,flows the AI builds are auto-arranged into clean columns.
  • :artist_palette: Device icons ,change a device’s icon straight from chat.
  • :electric_plug: Latest MCP spec (2025-11-25) with server-initiated requests (confirmations + progress).

Plus dozens of reliability, correctness and security fixes under the hood.

Hi, (text below is made by Claude: I have been trying te setup MCP Bridge with help from Claude as I am not that tech savvy to do this by myself :smile: )

I’m trying to connect MCP AI Bridge to Claude web (claude.ai) using the Remote Access / OAuth feature. My setup:

  • Homey Pro
  • MCP AI Bridge v3.0.0
  • Cloudflare Tunnel to port 52199, publicly accessible via HTTPS
  • Remote access enabled, public URL set correctly
  • API key configured under Security

The OAuth metadata endpoints are working correctly:

  • /.well-known/oauth-protected-resource → returns correct JSON :white_check_mark:
  • /.well-known/oauth-authorization-server → returns correct JSON with all endpoints :white_check_mark:

However, when Claude attempts to connect, it fails with:

Cannot authorize — invalid_request: Invalid request

Checking the /register endpoint (which is listed in the oauth-authorization-server metadata as registration_endpoint) via GET returns {"error":"Not found"}.

My assumption is that Claude performs dynamic client registration via POST to /register as part of its OAuth flow, and this step is failing — causing the “invalid_request” error before the authorization screen can complete.

Is dynamic client registration (RFC 7591) supported in v3.0.0? If not, is it on the roadmap? Claude web appears to require it to complete the OAuth handshake.

Thanks in advance!

@DGalama
Thanks for the incredibly detailed report, that made this easy to pin down.

Good news and a fix:

Dynamic Client Registration (RFC 7591) is supported in v3.0.0. Registration happens via POST /register, and I verified it returns a proper client_id. The {“error”:“Not found”} you saw is just because you hit it with GET (the endpoint is POST-only), so that part is a red herring, not the failure.

The actual bug is one step later, at the approval screen. When you approve the connection, the consent form was not sending the response_type parameter back to the server. The server re-validates the request at that point and requires response_type=code, so the final step failed with exactly invalid_request: Invalid request, right before the handshake could complete. I reproduced your error end to end and confirmed it.

I’ve fixed it: response_type is now carried through the consent step, and I verified the full flow (register → authorize → approve → token) now completes and issues a valid token. It’ll be in the next build (3.0.1), no config change needed on your side, just update and reconnect.

I’ll reply here again the moment the build is live. Thanks again for the clear write-up, and apologies for the hiccup!

Hi Danny,

Thanks for the quick fix! I updated to v3.0.1 and the authorization screen now appears correctly.

However, after filling in the API key and clicking “Approve access”, nothing happens. No redirect, no error, no visible feedback. I tested this on both desktop (Chrome and Safari) and Android, same result on both.

I checked the connector status in Claude afterwards: it is not connected, so the flow did not complete silently.

@DGalama
Found it, and fixed. :raising_hands:

The approval screen was working, but the page’s security policy (form-action 'self') also applied to the redirect after you click Approve, so Chrome and Safari silently blocked the jump back to Claude’s callback, no redirect, no error, exactly what you saw. The consent page now also allows the connector’s validated redirect address, so approval completes and the connection finishes.

This is in v3.0.2. I also ran the entire remote flow end to end this time (register → authorize → approve → token, then actually using the token for live calls, plus token refresh and read/write scoping, 24 checks), so it’s properly verified now rather than fixed blind. Just update to 3.0.2 and reconnect, no config change needed.

Thanks again for the great, precise reports, they made both fixes quick. Let me know if anything else comes up!

Tested and it worked! Fantastic job (and fast!).

Oh, and I’ll pass on your thanks for the reports to my friend Claude. :wink:

Hi,

I found out that Claude was struggeling with note colors for some reason. It was always making transparant note cards (I hope this helps). Thanks for the app!

The feedback and bug report to make this better for claude:

Homey MCP bridge — Advanced Flow note cards: wrong shape and invalid colours both fail silently

App version: MCP AI Bridge 3.0.2
Homey: Homey Pro (Early 2023), firmware 13.4.0
Reported by: an LLM agent building Advanced Flows through the bridge

Summary

Two separate mistakes an agent can make when adding note cards to an Advanced Flow are accepted
without complaint, stored, and reported as success — while the result on screen is wrong:

  1. Note text placed in args produces a blank note. The text and colour must be top-level
    value and color fields on the card; anything in args is stored but never read.
  2. An invalid colour produces an unstyled grey note. Homey stores any string at all — including
    "banana" and "#ff0000" — and the web app falls back to a default when it does not recognise
    the value.

Neither is caught by advanced_flows_validate, which returns valid: true with zero warnings for
both. Neither produces an error at write time. The write tools return only {success: true, flow_id},
so nothing in the response contradicts the agent’s assumption that it worked.

This matters more for an MCP server than for a human-facing API. A person adds a note and sees
immediately that it is blank. An agent cannot see the canvas. It has no feedback channel other
than the tool result, so a silent visual failure is invisible to it, and it will confidently report
that the flow was created correctly. In practice the mistake surfaced only when the user opened the
editor and asked why the notes were empty — and later, why they were grey.

Reproduction

A flow created through the API with six note cards, then read back verbatim:

card sent stored
n1 color: "orange" color: "orange"
n2 color: "purple" color: "purple"
n3 color: "green" color: "green"
n4 color: "banana" color: "banana"
n5 color: "#ff0000" color: "#ff0000"
n6 args: {value, color} color: undefined, args kept as an opaque blob

Every value round-trips unchanged. There is no validation and no normalisation anywhere in the path.
On screen, n1, n2, n4 and n5 render as grey default notes, and n6 renders blank.

advanced_flows_validate accepts the n6 shape as valid with no warnings, because it checks graph
structure — node ids, dangling edges, flow-card nodes missing a uri — and never per-type field
shapes.

Which colours are actually valid

This could not be determined from any documentation. The Homey feature page says only that notes
exist and “you can even change their color”, without listing the options, and the developer
documentation does not cover note cards at all.

It was established empirically instead, by scanning every note in one household’s hand-authored
flows — notes made in the official editor, therefore valid by construction:

200 notes across 67 advanced flows
  yellow      181
  red          13
  green         4
  blue          1
  (undefined)   1

So yellow, red, green and blue are known good, and undefined is a valid default. orange
and purple are confirmed not to render.

That method is the problem in miniature. The set had to be inferred from user data, it can only ever
be a lower bound, and an early guess based on a smaller sample got it wrong — green was assumed
invalid because the first sample happened to contain none. An agent has no way to discover this set,
so it guesses from ordinary colour names, and roughly half of those guesses fail silently.

Why this happens

Homey’s own API is fully permissive here, so the bridge is faithfully passing through a call that
Homey itself will not reject. The rendering contract lives only in the web app, which reads
card.value and card.color and falls back when they are missing or unrecognised.

The bridge’s tool description contributes to the first mistake. It lists note as a valid node type
and documents args generically as “object with card arguments”, giving an explicit example only for
delay. Notes are the one node type whose content lives outside args, and nothing says so — so
the generic instruction actively points the wrong way.

Suggested fixes

In rough order of value:

  1. Put the colour set in the tool’s JSON Schema as an enum. This is the strongest fix and the
    cheapest to maintain, because the schema is already in the agent’s context — it does not require a
    discovery call the agent has to know to make, and an invalid value becomes impossible to write
    without noticing. Any field with a closed set of values deserves the same treatment; a free-form
    string in a schema is a promise that any string is meaningful.
  2. Normalise the note shape in the bridge: lift args.value and args.color to top level for
    note nodes. The bridge already derives ownerUri from uri — this is the same class of
    ergonomic fix, and it makes the most natural-looking call correct instead of silently wrong.
  3. Warn in advanced_flows_validate and at write time for an unrecognised note colour, and for a
    note carrying args. The message should name the valid values, so an agent that hits it is told
    the answer rather than left to guess again.
  4. Echo the stored card back from write calls, or document that advanced_flows_get should be
    used to confirm. This catches the whole class of silent mangling rather than only colours — the
    agent can compare what it intended against what was kept.
  5. Document the note shape in the advanced_flows_create description, alongside the existing
    delay example.

Fix 1 alone would have prevented the colour half of this entirely, and fix 2 the other half.

Note

The bridge handled everything else in this session correctly, including a fairly large flow built
from scratch, a cards round-trip used to repair that flow in place, and reading back state to
verify each change. The cards round-trip in particular worked exactly as documented — edges and
input arrays survived untouched — which is what made repairing the notes a one-call fix once the
correct shape was known.

The exact full palette is still unconfirmed: yellow, red, green, blue are observed valid and
orange, purple observed invalid, but the authoritative list is presumably in the web app source
and would be worth reading off directly before writing it into a schema.

@LionelDerBoven
This is one of the best bug reports I’ve ever received, thank you. You nailed both the cause and why it matters more for an agent than a human. Fixed in v3.0.3:

  • Text in args → blank note: the bridge now lifts a note’s text and colour to the top-level value/color that Homey actually renders from, so the natural { args: { value, color } } shape works instead of coming out blank.
  • Invalid colour → grey note: the tool schema now carries the colour set as an enum (yellow, red, green, blue), so an assistant picks a valid one from its own context, and both advanced_flows_validate and the create/update result now warn about an unknown colour, a blank note, or content in args, naming the valid values so it’s told the answer instead of guessing again.
  • The note shape is now documented in the advanced_flows_create description alongside the delay example.

On the palette: you’re right that it’s undocumented and the API is fully permissive, so I went with your hand-authored set (yellow, red, green, blue). I also scanned a second household’s flows to corroborate; it added only orange, but those turned out to be agent-written (i.e. grey), which matches your finding. If the authoritative web-app list turns out larger, I’ll extend the enum, but this closes the silent-failure for the common cases.

Thanks again, genuinely useful. :folded_hands:

Hi, Here I am again with a feature request. Right now Claude code struggles creating dashboards. Would creating dashboards trough the MCP server be possible? I tested it with a local API key with claude and it can do things but it struggels with knowing what widgets to use:

Feature request: expose write access to Homey’s native Dashboards

Summary

The server currently exposes homey_dashboards_list, which wraps
ManagerDashboards.getDashboards() and is documented as read-only. Homey’s Web API supports
full dashboard CRUD, so the remaining methods could be exposed as well. Without them, a native
Homey Dashboard cannot be created or modified through this MCP server, even though the
underlying platform allows it.

Current behaviour

homey_dashboards_list works and returns {"items": [], "count": 0}. There is no
corresponding create, update or delete tool, so an agent can observe native dashboards but
never author one.

Why this matters

Homey’s per-user homescreen is personal: it cannot be shared with other household members,
duplicated, or reused. Native Dashboards can. Migrating a curated homescreen layout to a
Dashboard is a natural agent task — read the devices, flows and moods, then lay them out — but
it stops at the last step because the write side is missing.

The general gap: this server can already author devices, flows, advanced flows, moods, logic
variables, zones and scripts. Dashboards are the one first-class Homey object that is
read-only here.

Proposed API surface

Mirroring the naming of the existing tools:

Tool Wraps Homey endpoint Scope
homey_dashboards_get getDashboard({id}) GET /api/manager/dashboards/dashboard/:id homey.dashboard.readonly
homey_dashboards_create createDashboard({dashboard:{name, columns}}) POST /api/manager/dashboards/dashboard homey.dashboard
homey_dashboards_update updateDashboard({id, name?, columns?}) PUT /api/manager/dashboards/dashboard/:id homey.dashboard
homey_dashboards_delete deleteDashboard({id}) DELETE /api/manager/dashboards/dashboard/:id homey.dashboard
homey_dashboards_list_widgets getAppWidgets() GET /api/manager/dashboards/widget homey.dashboard.readonly

ManagerDashboards is present on both HomeyAPIV3Cloud and HomeyAPIV3Local, so this works
for locally-connected Homey Pro as well as cloud sessions.

Reference: https://athombv.github.io/node-homey-api/HomeyAPIV3Cloud.ManagerDashboards.html

Implementation notes

  1. The manager is reachable the same way the existing homey_dashboards_list reaches it — no
    new client, transport or auth path is needed. Only the write methods and the two extra read
    methods need wiring up.

  2. getAppWidgets() is the piece that makes the write tools usable. Without a widget
    catalogue an agent has to guess widget identifiers and settings keys. Please return each
    widget’s id, name and settings schema. If the full catalogue is large, a compact flag
    (id + name only) matching the convention of devices_list would help.

  3. The columns layout structure is undocumented upstream. Passing it through verbatim as an
    opaque JSON value is fine, and probably preferable to modelling it — agents can learn the
    shape by reading an existing dashboard back. Documenting one worked example in the tool
    description would save a lot of trial and error.

  4. Suggest homey_dashboards_update be a true partial update (omitted fields left untouched),
    so iterating on a layout does not require resending the whole dashboard and a dashboard
    keeps its id across edits.

  5. Delete is destructive and hard to undo from an agent’s position — worth putting behind the
    same guardrail treatment as the other destructive tools in this server.

Workaround in the meantime

homeyscript_run exposes the full Web API client server-side, so
Homey.dashboards.createDashboard(...) is reachable today. That works, but it routes a
first-class platform object through an escape hatch, and the method list is not discoverable
via Object.keys() — the prototype chain has to be walked to find it.

Environment

  • Homey Pro (Early 2023), firmware 13.4.0, local platform
  • homey_dashboards_list present and functional; no dashboard write tools available

Thanks you!

@LionelDerBoven

Done, and thank you for such a well-scoped request. Shipped in v3.1.0, exactly the surface you proposed:

  • homey_dashboards_get, homey_dashboards_create, homey_dashboards_update, homey_dashboards_delete, and homey_dashboards_list_widgets.
  • Widgets are discoverable: homey_dashboards_list_widgets returns each widget’s id, owner app and full settings schema (with a compact flag for id + name only), so an agent knows what it can place instead of guessing.
  • Update is a true partial update: it fetches the current dashboard, merges the fields you pass, and writes it back, so omitted fields survive and the dashboard keeps its id across edits.
  • Delete is treated as destructive: blocked when no API key is set, and it asks for confirmation on clients that support elicitation, same as the other destructive tools.
  • On the layout: it’s undocumented upstream, so I dug out the exact shape and put a worked example in the create tool description: columns: [{ id: <uuid>, widgets: [{ id: <uuid>, uri: "<widget id from list_widgets>", settings: {} }] }], each column and widget needing its own generated UUID. I verified the whole round-trip live (create empty, add a widget via update, read it back). Reading a dashboard back to iterate works exactly as you suggested.

No more routing a first-class object through the HomeyScript escape hatch. It reaches ManagerDashboards the same way the existing list tool does (local Homey Pro included). Thanks again, genuinely great write-up. :folded_hands:

Thanks! Will test later / tomorow!

Also claude was struggling with finding broken flows (deleted device cards for example).

This is the report.

Sorry for all the reports. I really appreciate the updates! I am using it a lot to “spring clean” my flows and automations right now!

Homey MCP — broken-flow detection returns false negatives on every affected flow

Date: 2026-08-08
Component: Homey MCP server — flow integrity tools
Severity: High (silent failure in a diagnostic tool)


Summary

flows_get_broken is documented as listing flows that are broken “when a card it uses no longer
exists, for example after a device or app was removed”. In practice it appears to pass through
Homey’s own flow.broken property rather than resolving card references itself. That property was
false for every affected flow on this installation — including a flow constructed specifically to
be broken — so the tool returns an empty list and the user is told everything is fine.

This is a silent failure in a diagnostic tool, which is the worst place for one: a clean result is
indistinguishable from a working check. The same root cause affects two other surfaces.

Metric Value
Flows scanned 99
Real problems found by manual cross-reference 4
Reported by the MCP 0
Detection rate 0%

The fix is cheap. getFlowCardTriggers(), getFlowCardConditions() and
getFlowCardActions() already return the fully-resolved live card registry — 2,559 manager/app
cards plus 3,157 device cards
on this Homey. Validating a flow is a set membership test per card.
No new API surface is required.


Environment

Homey Homey Pro
Devices 148
Flows 26 basic + 73 advanced = 99
Cross-check Flow Checker 1.37.2 (Martijn Poppen)

Reproduction

  1. Create an Advanced Flow using one device for a trigger, a condition and an action.
  2. Delete that device from Homey.
  3. Call flows_get_broken.

Expected: the flow is listed, ideally with the three offending cards.
Actual: {"count": 0, "broken": []}

The reference flow used here is Test Broken flow cards
(4a537833-b40e-4727-b5f9-8b7a1f392bc1). All three of its cards reference the deleted device
a3532f0a-635e-4048-b225-1fe04153ebb3, confirmed deleted — devices_get on that id returns
Not Found.

Three surfaces, same flow, same wrong answer

Surface Response Verdict
flows_get_broken {"count": 0, "broken": []} False negative
advanced_flows_listbroken false False negative
advanced_flows_validate {"valid": true, "errors": [], "warnings": []} False negative
Manual reference resolution 3 cards → deleted device Correct

Individual defects

B1 — flows_get_broken does not resolve card references [CRITICAL]

The core defect. Returns an empty list on an installation with four genuinely broken flows. Because
it inherits flow.broken, it only ever reports what Homey itself already decided — and Homey did
not set that flag for any deleted-device case observed here.

Independent confirmation that the breakage was real: the Flow Checker app, running against the same
Homey, did flag one of these flows under “Kapotte ingeschakelde flows” while the MCP reported zero.

B2 — broken field in list tools is always false [MISLEADING]

flows_list and advanced_flows_list both expose a broken boolean. Across all 99 flows it was
false — including the intentionally broken one. A field that is structurally incapable of being
true is worse than no field, because it implies a check was performed. Either populate it from a
real check or drop it.

B3 — advanced_flows_validate validates graph shape only [GAP]

Passing the exact card map of the broken reference flow returns valid: true with no errors and no
warnings. The tool checks ids, edges and reachability, but never asks whether a card’s uri
actually exists. Since its stated purpose is to catch problems before creating a flow, this is
where a bad device reference should surface first.

Suggested: add a resolve_references option (default on) that reports unknown card uris as errors
and app-disabled cards as warnings.

B4 — No reference check for args.flow.id (deleted sub-flows) [CRITICAL]

This class is missed by everything, including the Flow Checker app. A programmatic_trigger
action stores its target as:

{"flow": {"id": "c93cbe0c-3d5a-4aca-9004-bd5facce54ff",
          "name": "Module-buiten-motion",
          "type": "advanced"}}

That flow was deleted. Three such cards across two enabled flows pointed at it. Nothing warned the
user, and the card still renders the stale cached name, so the editor looks correct. For users who
build modular flows out of sub-flows this is the most damaging failure mode, and the one with the
best payoff: the cached name is right there in the args and makes an excellent error message.

B5 — Installed-but-disabled apps are not treated as breakage [GAP]

An app can be installed while enabled: false / state: "stopped". Its cards then vanish from the
live registry and any flow using them silently never fires — but the app is still “installed”, so an
installed-check passes. Two enabled flows here depended on cards from a disabled app. This should be
reported as recoverable breakage, distinct from a deleted device.


What a correct check must resolve

Five checks cover every failure mode observed. Each is a lookup against data the API already returns.

# Check Method Catches
C1 Device still exists Parse homey:device:<uuid>:<card>, test uuid against getDevices() Deleted devices
C2 Card definition still exists Test full card id against the union of the three card registries Changed devices, app updates that drop cards, disabled apps
C3 Referenced flow still exists Test args.flow.id against basic + advanced flows Deleted sub-flows (B4)
C4 Owning app is runnable For homey:app:*, check enabled, state, crashed — not merely installed Disabled and crashed apps (B5)
C5 Argument references resolve Resolve UUID-shaped args values against devices, zones, users, moods, logic variables, flows Dangling zone / variable / user references

C2 is the highest-value check. The registry covers device cards too — all 3,157 of them — so it
catches a device that still exists but lost a capability after a driver or firmware update. That is
the “device changed” case, as opposed to “device deleted”.

On false positives. A naive UUID sweep over card args produced 120 hits on this installation,
of which 108 were legitimate references to users, moods and logic variables. Resolving against all
six entity types brought it to 12, and every one was real. C5 is only worth shipping with the
full resolution set.


Suggested response shape

Flow-level granularity is not actionable — the real-world case here was an Advanced Flow with 40
cards. Report the card, its type and its canvas coordinates so the user can find it. Separate hard
breakage from recoverable, and enabled flows from disabled ones, so a deliberately parked flow does
not read as an incident.

{
  "count": 2,
  "broken": [
    {
      "flow_id": "a31aab5d-d003-43fe-83f0-fd1e7176ebea",
      "flow_name": "Knopen control center gang inkom",
      "type": "advanced",
      "enabled": true,
      "severity": "broken",
      "cards": [
        {
          "card_id": "homey:device:3e276abd-...:on",
          "card_type": "condition",
          "x": 400, "y": 980,
          "reason": "DEVICE_MISSING",
          "missing_ref": "3e276abd-6386-46fd-b11a-e4de88d39166"
        }
      ]
    },
    {
      "flow_id": "88a742e5-d2f3-4484-b2ed-6d5774312260",
      "flow_name": "Buiten verlichting auto",
      "type": "advanced",
      "enabled": true,
      "severity": "broken",
      "cards": [
        {
          "card_id": "homey:manager:flow:programmatic_trigger",
          "card_type": "action",
          "x": 800, "y": 840,
          "reason": "FLOW_MISSING",
          "missing_ref": "c93cbe0c-3d5a-4aca-9004-bd5facce54ff",
          "cached_name": "Module-buiten-motion"
        }
      ]
    }
  ]
}

Useful reason values: DEVICE_MISSING, CARD_MISSING, FLOW_MISSING, APP_MISSING,
APP_DISABLED, APP_CRASHED, ARG_UNRESOLVED.

Useful severity values: broken (unrecoverable without editing) and degraded (recoverable, e.g.
re-enable the app).


Reference implementation

The check that found all four problems. Run as HomeyScript against the live Homey. Roughly 40 lines
of actual logic; the bulk is assembling the resolution sets.

const devices = await Homey.devices.getDevices();
const apps    = await Homey.apps.getApps();
const flows   = await Homey.flow.getFlows();
const advs    = await Homey.flow.getAdvancedFlows();
const zones   = await Homey.zones.getZones();
const users   = await Homey.users.getUsers();
const moods   = await Homey.moods.getMoods();
const vars    = await Homey.logic.getVariables();

// C2: the live card registry — includes device cards
const t = await Homey.flow.getFlowCardTriggers();
const c = await Homey.flow.getFlowCardConditions();
const a = await Homey.flow.getFlowCardActions();
const registry = new Set([
  ...Object.keys(t), ...Object.keys(c), ...Object.keys(a)
]);

const devIds = new Set(Object.keys(devices));
const known  = new Set([                       // C5 resolution set
  ...devIds, ...Object.keys(apps),  ...Object.keys(zones),
  ...Object.keys(flows), ...Object.keys(advs), ...Object.keys(users),
  ...Object.keys(moods), ...Object.keys(vars)
]);
for (const f of Object.values(advs))           // advanced-flow node ids
  Object.keys(f.cards || {}).forEach(id => known.add(id));

function checkCard(id, args, report) {
  if (typeof id !== 'string') return;
  if (/^homey:(manager:logic:(all|any)|advancedflow:)/.test(id)) return;

  const parts = id.split(':');

  // C1 — device deleted
  if (parts[1] === 'device' && !devIds.has(parts[2]))
    return report('DEVICE_MISSING', parts[2]);

  // C2 — card definition gone (covers C4: disabled apps drop out of registry)
  if (!registry.has(id))
    return report('CARD_MISSING', id);

  // C3 / C5 — argument references
  for (const [key, val] of Object.entries(args || {})) {
    const ref = (val && typeof val === 'object') ? val.id
              : (typeof val === 'string' ? val : null);
    if (!ref || !/^[0-9a-f-]{36}$/i.test(ref)) continue;
    if (!known.has(ref))
      report(key === 'flow' ? 'FLOW_MISSING' : 'ARG_UNRESOLVED', ref, val.name);
  }
}

Iterate basic flows over trigger / conditions / actions, advanced flows over
Object.values(flow.cards), skipping type: "note" nodes.


Secondary observations

No flows_validate for basic flows

advanced_flows_validate has no counterpart for basic flows, so there is no dry-run path at all
before flows_create. Worth adding alongside the B3 fix, sharing the same resolver.

Verified working

For balance: everything used to build this report behaved correctly. devices_list, devices_get,
apps_get, apps_get_settings, flows_get, advanced_flows_get and homeyscript_run all
returned accurate, complete data. devices_get returning a clean Not Found for the deleted uuid
is what made the diagnosis straightforward. The defect is narrowly in the flow-integrity surfaces,
not the underlying API access.


Compiled from a full scan of 99 flows on a live Homey Pro, 8 August 2026. Findings verified by
re-scan after user fixes and cross-checked against Flow Checker 1.37.2. No flows were modified
during analysis.

@LionelDerBoven
This is an exceptional report, thank you, the reference implementation made it a joy to build. Fixed in v3.1.2, addressing the whole thing:

  • B1: flows_get_broken no longer trusts Homey’s broken flag. It now resolves every card against the live registries: device exists (C1), card definition in the trigger/condition/action registry (C2), referenced sub-flow exists via args.flow.id (C3), owning app is runnable (C4), and UUID args resolve against devices/zones/users/moods/variables/flows (C5).
  • Response shape is exactly what you proposed: per-card card_type, x/y, reason (DEVICE_MISSING, CARD_MISSING, FLOW_MISSING, APP_MISSING, APP_DISABLED, APP_CRASHED, ARG_UNRESOLVED) and severity (broken vs degraded), plus cached_name for deleted sub-flows. Added include_degraded and enabled_only filters so a parked flow doesn’t read as an incident.
  • B3: advanced_flows_validate gained resolve_references (default on), sharing the same resolver, so a bad device/app/flow reference surfaces before the flow is created.
  • B4 / B5 are covered: deleted sub-flows report FLOW_MISSING with the cached name, and disabled/crashed-app cards report APP_DISABLED/APP_CRASHED as degraded.
  • B2: I removed Homey’s unreliable broken flag from the flow-list tools, since a field that can’t be true is worse than none.

I tested it against a live home: it found 14 genuinely broken flows that the old tool reported as zero, with no false positives (it correctly resolved against 87 logic variables etc., and only flagged references that are truly gone). The one thing I left for a follow-up is a basic-flow flows_validate counterpart, happy to add it next. Never apologise for reports like this, they make the app better. :folded_hands:

MCP AI Bridge 4.0 is out

You can find it in the TEST release!

This is the biggest release since 3.0, and the short version first: if you already have a connector set up, nothing changes and nothing needs touching. Everything below is either new ground or a hole that got closed.

A new protocol, alongside the old one

MCP got a new specification (2026-07-28) that moves the protocol to a stateless model. Version 4.0 speaks both eras side by side. A modern client is recognised from its own request and gets the new path; everything else keeps the exact handshake it has always used. There is no setting for this and no migration step.

What the new era brings:

  • Confirmations that cannot be moved or reused. When an irreversible action needs your approval, that approval now travels through the client itself, cryptographically tied to the specific tool, the specific arguments and the caller. A confirmation for deleting one device can never be replayed against another device, and never a second time.
  • Slow work can run in the background. A long job hands back a handle immediately instead of holding the connection open, and the client collects the result later. Opt-in, off by default.
  • A live notification channel replacing the old subscribe mechanism.
  • Client registration via a metadata document, replacing the now-deprecated dynamic registration.

Security

Most of this came out of a long adversarial audit run, where the goal was specifically to break the thing rather than confirm it worked. Findings worth naming:

  • A confirmation prompt could be skipped entirely by wrapping the call in a batch. Refused now.
  • Flooding the server with declined confirmations could resurrect an accepted one and run an irreversible action a second time. The store fails closed instead.
  • A client could opt out of being asked simply by not declaring that it could show a prompt.
  • Web pages on other sites could reach the server through DNS rebinding. Origins are validated now.
  • A registration flood could quietly unregister a connector you were still using.
  • A corrupt guardrails setting used to disable every rule you had configured, silently. It now drops to read-only and tells you.

Two new settings

Both off by default, so nothing changes unless you choose it.

Refuse when nothing can be asked. Some connections have no way to show you a prompt: the REST endpoint, and clients that only send without a return channel. Today those run reboot, delete and uninstall without asking. Turn this on and they get refused instead. Worth knowing before you flip it: connectors that rely on the current behaviour will start getting an error for exactly those three things.

Allow slow work in the background. Enables the task handling described above. Only clients that ask for it will use it.

Also

Cancelling background work now genuinely skips work that has not started yet, and a finished result is never discarded in silence.


As always: if something behaves oddly, post it here with what you were doing and which client you used. Every one of the bugs fixed in the 3.x line came from a report in this topic.

Thank you! it is already better!

It is still struggeling with this. Testing with 3.1.3:

Follow-up: expose Homey’s built-in dashboard widgets in homey_dashboards_list_widgets

Follow-up to the earlier request for dashboard write access. That part shipped and works
this is about the one piece that did not make it in, which turns out to be the piece that
actually gates automation.

Everything below was verified against a local Homey Pro on firmware 13.4.0 using the newly
released tools.

What shipped, and works

All five tools behave as documented. Verified by real calls, not inspection:

Tool Result
homey_dashboards_create Creates the dashboard, returns the stored object
homey_dashboards_get Returns columns verbatim; round-trips byte-for-byte
homey_dashboards_update True partial update — reports updated:["columns"] when only the layout changed, and updated:["name","columns"] when both did. Dashboard keeps its id across edits.
homey_dashboards_list Picks up dashboards created out-of-band
homey_dashboards_list_widgets Returns the app widget catalogue, compact flag works

A two-widget layout written with update and read back with get came back identical. The
columns schema in the tool description matches the server’s behaviour exactly. Nothing to fix
here.

What is still missing

homey_dashboards_list_widgets returns only widgets contributed by installed apps. On the
test system: 20 entries, every single one of the form homey:app:<appId>:<widgetId>.

Homey’s built-in widgets are not in the list: the device tile, flow button, mood/scene
button, zone tile, weather, insights chart, and whatever the timeline equivalent is. These are
the widgets an ordinary dashboard is mostly made of.

Re-probed after the update, all still 404 or unchanged:

GET /api/manager/dashboards/appwidget?includeBuiltin=true   -> same 20, app widgets only
GET /api/manager/dashboards/appwidget?builtin=true          -> same 20, app widgets only
GET /api/manager/dashboards/widget                          -> 404
GET /api/manager/dashboards/widgets                         -> 404
GET /api/manager/dashboards/builtin                         -> 404
GET /api/manager/dashboards/builtinwidget                   -> 404
GET /api/manager/dashboards/systemwidget                    -> 404
GET /api/manager/dashboards/manifest                        -> 404
GET /api/manager/mobile/widget                              -> 404
GET /api/manager/apps/widget                                -> 404
GET /api/manager/system/widget                              -> 404

They appear to be defined client-side in the Homey app rather than served by the platform.

Why this blocks the use case

The motivating task was migrating a personal Homey homescreen — which is per-user and cannot
be shared or reused — into a native Dashboard, which can.

With write access but no built-in widget URIs, that dashboard can be assembled only from app
widgets. On the test system that covers roughly six tiles out of forty-plus. Everything that
makes the layout worth migrating — status toggles, thermostat, blinds, media, locks, eleven
favourite flows, fifteen moods, zone tiles, weather — is built-in, and therefore unreachable.

So the write tools are currently sufficient to create a dashboard, but not to create a useful
one, unless a human first builds a sample by hand so the URIs can be read back out of it.

The silent-failure problem this creates

uri and settings are still passed through without validation. Verified after the update:

uri:      "homey:app:this.app.does.not.exist:nonsense-widget"
settings: { "bogus": true }
result:   { "success": true, "updated": ["columns"] }

A non-existent app, a widget id that was never defined, and an invented settings key are all
accepted and stored. Nothing surfaces until someone opens the dashboard on a phone and finds
blank tiles.

This matters more than it would otherwise, because the missing catalogue pushes callers toward
guessing built-in URIs — and guessing is exactly what fails silently here. The two issues
compound.

Requests, in order of value

  1. Include built-in widgets in homey_dashboards_list_widgets. Ideally behind a flag
    (include_builtin: true) so existing callers are unaffected. Each entry needs the same
    shape the app widgets already have: id/uri, name, whether it takes a device, and the
    settings schema. This single addition unblocks the whole use case.

    If the platform genuinely does not expose them, a static table maintained in the server
    would be just as good in practice. Built-in widget ids change rarely, and a slightly stale
    table beats no table.

  2. Validate uri against the known widget set before writing, and return an error listing
    the unknown URIs. If the built-in list is unavailable and validation would produce false
    rejections, then warn rather than reject — but say something. Silent acceptance of a
    dashboard that will render blank is the worst of the options.

  3. Document the fallback in the tool description, for as long as 1 is not done: build one
    dashboard by hand in the Homey app containing one widget of each built-in type, then call
    homey_dashboards_get on it to harvest the URIs and settings shapes. It works, but it
    requires a human round-trip in the middle of what is otherwise an automatable task, and
    nothing currently tells a caller that this is the required workflow.

Environment

  • Homey Pro (Early 2023), firmware 13.4.0, local platform
  • Dashboard write tools present and functional
  • homey_dashboards_list_widgets returns 20 widgets, all app-provided
  • Homey MCP AI bridge 3.1.3

@LionelDerBoven
Thank you for this. The probing you did saved me most of the
investigation, and it turned out to be right on the part that matters.

Shipped in 4.0.1.
4.0.1 is ready on the TEST version

The built-in widgets: you were right, and here is the proof

I went looking for an endpoint you might have missed. There was exactly one
candidate, and it does not deliver.

ManagerDashboards in the homey-api specification exposes thirteen operations
in total:

createDashboard, deleteDashboard, getDashboard, getDashboards, updateDashboard,
getState, getAppWidget, getAppWidgets, getAppWidgetAutocomplete,
getWidgetStore, getWidgetStores, getWidgetStoreState, setWidgetStoreState

Every path you probed returns 404 because it genuinely does not exist. But
/widgetstore does exist and neither of us had tried it, so I wired it up
behind a new include_builtin: true flag, installed it and called it against a
live Homey. It comes back empty. HomeyScript’s Homey.dashboards is a stub with
nothing but a constructor.

So: built-in widgets are defined client-side in the Homey app and are not
reachable from any API. I did not add a static table, for the reason you
identified yourself: a guessed uri fails silently, and a table I could not
verify would be guessing dressed up as a catalogue.

What include_builtin: true does now is say this outright, and give the
workaround, instead of returning a list that looks complete.

Silent acceptance is fixed

This is where your report paid off most. Your example uri names an app, and app
widgets can be checked authoritatively. So they now are, before anything is
written:

homey_dashboards_create(uri: “homey:app:this.app.does.not.exist:nonsense-widget”)

Error: Unknown app widget uri: homey:app:this.app.does.not.exist:nonsense-widget.
These name an installed app and a widget it provides, so they can be checked,
and none of these match. Call homey_dashboards_list_widgets for the valid ids.
Nothing was written.

Verified on a live Homey: refused, and the dashboard count stayed where it was.

A uri that is not homey:app:* is almost certainly one of the built-ins, and
rejecting those would be a false negative on legitimate input. Those go through,
with a warning in the response naming each unverifiable uri. Your framing was
the deciding argument here: silent acceptance is the worst option, so the choice
was between refusing and warning, not between refusing and saying nothing.

One deliberate limit: if the widget catalogue is unreachable, validation is
skipped rather than failing the write. A transient API blip should not break
dashboard editing.

Documented

homey_dashboards_create and homey_dashboards_update now both state that
built-in widgets are not listed, why, and the harvest workflow: build one
dashboard by hand containing the widget types you need, then read the URIs and
settings shapes back out with homey_dashboards_get.

It still needs that human round-trip. I could not remove it, only stop the
tools from pretending it is not there.

:tada: New since 4.0

Four releases of protocol work, hardening and new capabilities. The short version.

:shield: Confirmation actually works now

Asking you to confirm a reboot, a device delete, an app uninstall or a dashboard delete was presented as a headline feature, and on a normal setup it did not happen. Most assistants cannot show a prompt, and those connections were simply waved through. Three separate ways around the prompt were found and closed, including one where wrapping the call in a list was enough.

It is now on by default. A connection that cannot ask you is refused, and the error names the tool and where to change the setting. If you deliberately switched it off before, it stays off.

:locked: Security settings fail the safe way

A typo in the access mode, or a key stored with an unrecognised permission, used to grant full access. Anything unreadable now means the least access.

Access mode “off” really blocks everything. It used to stop tool calls while still handing out device names, zones and the values of your logic variables. Your assistant now sees an empty list and can tell you the bridge is switched off, instead of reporting a broken connection.

The check that stops hostile web pages reaching your Homey was comparing two values an attacker controls, so it stopped nothing. It compares against your Homey’s real addresses now.

:white_check_mark: The settings page tells the truth

Save, delete and clear buttons announced success whether or not anything happened, including the button that revokes an API key. If you revoked a key on an older version, revoke it again to be sure. The status bar also showed a green “server active” when the app could not be reached at all, which is exactly when you look at it.

:electric_plug: New protocol, old connectors untouched

The whole protocol layer was rebuilt for the 2026-07-28 MCP specification, alongside the old one. A modern client is recognised from its own request; everything else keeps the handshake it always used. Nothing you set up needs changing.

Confirmations in the new era are cryptographically bound to the exact tool and the exact arguments, so a yes for deleting one device can never be replayed against another, and works only once.

:mobile_phone: The Bridge is now a device

Add it from Devices and you get the server itself on your Homey: whether it is running, how many assistants are connected, tool calls today, failed calls today, refused attempts today, broken flows, the current access mode, and whether remote access is on.

Because it is a real device you get Insights graphs of all of that over time, a place on your own Homey dashboards, and flow triggers when a value changes. None of which the app could offer about itself before. Adding it is optional.

:shuffle_tracks_button: Flows the AI builds run in parallel

Assistants habitually chain action cards, which makes each one wait for the previous and stops everything after the first failure. Actions now start together. A sequence that is clearly deliberate is left alone: a wait card in the run, an action whose result feeds the next, or a branch off a false output. When something stays chained you are told why.

Flows the AI builds also get tidied into clean left-to-right columns.

:bar_chart: Also

Homey’s own native Dashboards can be created and edited by the AI. Widget URIs are checked before writing, so a typo no longer becomes a blank tile you find days later. Broken-flow detection resolves every card against your live system instead of trusting Homey’s own flag, which stays false for most deleted-device cards. Moods and zone activity are reachable. A tool you have blocked no longer appears in the list your assistant sees.

And older Homeys are now refused clearly on startup instead of crash-looping. See Requirements above.

@LionelDerBoven @Luke_Vredeveld New version (test)