Groups over MCP - #45
Merged
Merged
Conversation
A group has been usable only from inside Node-RED. Over MCP an assistant had to enumerate a dozen devices and do the arithmetic itself, and to switch a room it sent a dozen commands. get_groups and control_group are their own tools rather than group_id parameters on the device ones. The tool boundary is the explanation: a model reaches for control_group when the user says "all the lights" and set_light when they mean one lamp, without having to read a paragraph to work out which. It also keeps the schemas honest — a group has no items, so it has no business in one built around item_id. What the descriptions spend their words on is the asymmetry, because it is the part that is genuinely surprising: reading a group uses its state-capable members, commanding it uses its command-capable ones, and either set can be empty. Commanding never writes the value; the value follows from what the members report back. A group with nothing reporting has no value key at all rather than a null one, since a null reads as "the value is nothing" — a different claim from "nobody is answering". get_all_states gains an additive groups key. Without it a model could only discover groups by reading a description closely enough to call a second tool. Groups gain Tags to go with the Notes they have always stored and never shown. The name is usually all an assistant has, and "Alla lampor" does not say whether the outdoor lights are in it; get_groups takes a tag filter so it can ask for the set it means. The tag-parsing one-liner existed twice already, so it moves to hal.js rather than becoming three copies that can disagree. Verified live through the existing connector, which sees the new key without a reconnect: Temperatur inne reads 26.18 by average over 9 live members, correctly readable and not controllable, carrying the note that had never left the editor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four gaps worth closing, and one suggestion declined. control_group never said what it returns, so a caller had to guess between trusting a silent ack and re-reading "just in case". It now documents the shape in the description — but not the per-member success list the review asked for, because hal2 cannot know it: commands go fire-and-forget onto the event bus and are paced by the rate limit, so most members have not been sent when the call returns. An empty failures array would look like a promise. It says so instead, and says to read the group again to confirm. While there, the count became true rather than estimated. It reported the member list's command-capable count; the bus listener and the tool now share one send path, so what comes back is the queue that was actually built — a member whose thing has gone missing shows up in skipped rather than being counted as commanded. get_groups gains group_id, which control_group had and it did not — an exact id in hand had no direct way to re-read one group. ha_type on get_groups now expands the same category aliases as get_all_states, so ha_type="light" reaches a dimmer group here exactly as it reaches a dimmer device there. Documenting a difference would have been the worse of the two fixes: one parameter name meaning two things is a trap however well described. range is spelled out as highest minus lowest; percent true says 0-100. Declined: typing `value` as a string|number|boolean union. A colour group takes a structured value, so the union would reject a legitimate call — and since nothing validates against it, the only effect would be to mislead. The prose says what each ha_type expects and that the parameter is untyped on purpose. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The configured Value was the only question a group could answer. But the same members answer different ones depending on the function: "any true" says whether a lamp is on, "all true" says whether the command to turn them all on worked, "count true" says how many are. One group, three questions, and only one of them reachable. get_groups takes a function argument that computes another one from the same members for that call. It changes nothing: the configured Value is still what the flow nodes read and what get_all_states reports, and the reply names configured_function whenever the two differ so an ad-hoc reading cannot be mistaken for the setting. The larger consequence is that `readable` stops meaning "someone picked a function" and starts meaning what it says — the group has members that carry a state. Every one of this location's seven command groups turns out to have them: switchable devices report back. They were all being reported as unreadable because nobody had chosen a default for them, which is a poor reason to hide 39 members that were answering all along. An unknown function name is refused with the list rather than quietly returning nothing, and the schema's enum is checked against the module's own function table by test, so the two cannot drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The on-demand function is part of the same feature, not a patch on top of it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Asking a temperature group whether all its members are true came back as false. That is a claim about the group when it should have been a statement about the question: no member holds a boolean, so allTrue has nothing to be true of. It now comes back as an error naming what the members do hold and listing the functions that fit. Partial coverage is the opposite case and is answered rather than refused. On a mixed group — "Alla lampor" here is 29 switches and 10 dimmers — average uses the dimmers and ignores the rest, returning 25.4. That is a real answer to a real question, but presented as the group's value it reads as the brightness of the house. The reply now carries used beside live, so a mean over 10 of 39 members can be recognised as one. The split is what makes both honest: zero usable members is a refusal, some is a disclosure. usableCount and suitableFunctions live in the aggregation module, so what the tool claims about a function's applicability comes from the same code that would compute it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The README asked for it, and the sweep found the same thing in the MCP tool descriptions — where it does more harm. Those examples exist to teach a model a pattern, and half a pattern in a language it cannot assume it will see again teaches nothing: get_history explained that things and items are separate namespaces using a device called "Sjövatten Sensor" holding an item called "Temperatur", right next to the English word it is a translation of. Changed in text that ships: the get_history namespace example, two tag="ute" examples, the item_name and thing_name examples, and the group examples in get_groups and the README. docs/API.md regenerated from the catalog. Comments carrying the same names went with them, so the codebase reads one language. Left alone: scripts/bsky-announce.js, whose Swedish is a prompt asking a model for an English release note. It is the author talking, not the package, and it is excluded from the published tarball. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Groups gained a value of their own in 2.7.0, but only inside Node-RED. Over MCP an assistant still
had to enumerate a dozen devices and do the arithmetic itself, and to switch a room it sent a dozen
commands.
Two tools, not two parameters
get_groupsandcontrol_groupare their own tools rather thangroup_idparameters on thedevice ones. The tool boundary is the explanation: a model reaches for
control_groupwhen theuser says "all the lights" and
set_lightwhen they mean one lamp, without having to read aparagraph to work out which. It also keeps the schemas honest — a group has no items, so it has no
business in one built around
item_id.get_all_statesgains an additivegroupskey. Without it a model could only discover groups byreading a description closely enough to call a second tool.
devicesis untouched.What the descriptions spend their words on
The asymmetry, because it is the part that is genuinely surprising and no amount of schema conveys
it:
ones. Either set can be empty — a sensor group reads and cannot be commanded, a scene group is
the reverse — so
readableandcontrollableare separate flags rather than one.report back afterwards.
valuekey at all, not a null one. A null reads as"the value is nothing", which is a different claim from "nobody is answering".
switched.
Groups gain tags
Things and Items have always had tags and notes, and the catalog leans on them heavily. A group
needs them more, not less: the name is usually all an assistant has to go on, and "Alla lampor"
does not say whether the outdoor lights are in it. Both now reach MCP, and
get_groupstakes atagfilter so the assistant can ask for exactly the set it means.The tag-parsing one-liner already existed twice, so it moved to
resources/hal.jsrather thanbecoming a third copy that can drift.
Review round
Claude.ai used both tools unprompted on first contact and reviewed them. Four gaps closed:
control_groupnever documented its return. It does now — but not the per-member successlist that was asked for, because hal2 cannot know it: commands go fire-and-forget onto the event
bus and are paced by the rate limit, so most members have not been sent when the call returns. An
empty
failuresarray would look like a promise. It says so plainly and says to re-read thegroup instead. The count also became true rather than estimated: the bus listener and the tool
now share one send path, so what comes back is the queue that was actually built.
get_groupsgainedgroup_id, whichcontrol_grouphad and it did not.ha_typenow expands the same category aliases asget_all_states, soha_type="light"reaches a dimmer group here exactly as it reaches a dimmer device there. Documenting the
difference was the worse of the two fixes: one parameter name meaning two things is a trap
however well described.
rangeandpercent trueare spelled out in the function list.Declined: typing
valueas astring|number|booleanunion. A colour group takes a structuredvalue, so the union would reject a legitimate call — and since nothing validates against it, the
only effect would be to mislead.
Testing
276 passing.
lib/group-tools.jsholds the part with judgement in it — entry shaping, filtering,name resolution — so it is testable without a running Node-RED. The cases worth naming: a silent
group carries no
valuekey,readableandcontrollablevary independently in all fourcombinations, an ambiguous name is refused rather than guessed at, and a report-only group refuses
a command while listing the ones that would take it.
Verified live throughout: the
groupskey appears inget_all_stateswithout a connectorreconnect,
Temperatur innereads byaverageover 9 live members with the note that had neverleft the editor, and
ha_type=lightreturns the dimmer groups alongside the light ones.Follow-up: the function is not fixed
The configured Value was the only question a group could answer. But the same members answer
different ones depending on the function: any true says whether a lamp is on, all true says
whether the command to turn them all on worked, count true says how many are. One group, three
questions, and only one of them reachable.
get_groupstakes afunctionargument that computes another one from the same members for thatcall. It changes nothing — the configured Value is still what the flow nodes read and what
get_all_statesreports — and the reply namesconfigured_functionwhenever the two differ, so anad-hoc reading cannot be mistaken for the setting.
The larger consequence:
readablestops meaning "someone picked a function" and starts meaningwhat it says — the group has members that carry a state. Every one of this location's seven command
groups turns out to have them, because switchable devices report back. They were all being reported
as unreadable because nobody had chosen a default, which is a poor reason to hide 39 members that
were answering all along.