Conversation
That package was extracted from hal2 and has since gained work hal2 never got back. These two modules are pure and dependency-free, so they come across unchanged along with their 40 tests. claim-gate.js generalises authorization from a single claim value to comma-separated any-of lists composed with AND. mcp-rpc.js is the JSON-RPC dispatcher, separated from Express and Node-RED so the whole decision surface is testable; only hal2's standalone server will use it, since the embedded route's built-in catalog does not fit its registry shape. Two adaptations. core/mcp-auth.js takes only the functional delta — configurable localDebugGroups instead of a hardcoded ['admin'], so gates with other values can be exercised locally — keeping hal2's own header, which accurately describes it as extracted from eventhandler.js. And mcp-rpc.test.js points at hal2's admin catalog in core/mcp-tools.js rather than importing a lib/admin-tools module hal2 does not have, so the dispatcher is tested against the real get_flow/deploy_flow set instead of a copied duplicate. Nothing is wired up yet; 166 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The only per-tool distinction hal2 had was admin, so a token that could read state could also switch every light. This adds the sets a read/write gate can be keyed on. Two things make the classification worth stating explicitly rather than deriving. get_scenes and get_alerts are read-only but are handled inside dispatchControlTools, so dispatcher membership is not the answer. And control_light is an undocumented alias of set_light that the dispatcher accepts but MCP_TOOLS does not list — omitting it from the write set would be a clean bypass, so a test pins it. toolClass() fails closed: anything unclassified counts as a write, and a partition test turns a future unclassified catalog entry into a build failure rather than a silent hole. No gate reads these yet; 170 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The MCP surface had one per-tool distinction — admin — so any token that could read state could also switch every light. It now has three classes behind comma-separated any-of value lists on a single claim, composed with AND: read, write, and the existing admin. Where the check sits matters. node.callTool is the choke point for both the /mcp route and hal2Api, but only the former has claims this package verified: hal2Api takes msg.claims straight off the message, so a flow could self-grant. The gate is therefore built per request from verified claims and passed in opts, never derived from `claims` inside callTool. No gate in opts means no read/write gating, which is exactly the local-flow case. The admin check is untouched and stays in dispatchAdminTools, where it also guards hal2Api by requiring claims to be present at all — only its matcher gains list support. tools/list and the initialize blurb filter by the same gate, so a barred tool is never advertised and then refused; denials remain isError results carrying a reason, since MCP clients collapse a JSON-RPC error into "tool execution failed". The standalone server drops ~90 lines of inline dispatch for lib/mcp-rpc.js, which brings per-tool gates on hal2MCPIn with it. lib/common.js loses claimSatisfied — the last caller is gone and claim-gate covers its semantics, including the single-value form, so existing configs match identically. Hardening ported alongside: null-prototype tool registries, so a caller asking for "__proto__" gets an unknown-tool error rather than a 30 s hang; duplicate tool-name warnings and ownership-checked unregistration; and owner-tagged route removal so partial-deploying one of two servers sharing a path cannot strip its sibling. All new gate fields default to empty, which is no constraint — an existing install behaves exactly as before, pinned by a test that walks the whole catalog. 175 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The claim name moves out of the admin section: it is now shared by all three gates and had been hidden whenever admin tools were off. Read and write value lists sit beside it, hal2MCPIn gains its per-tool list with the live claim-name tip, and the standalone server's field is reworded for lists. Also wires localDebugGroups through the Event handler, which mcp-auth already accepted but nothing supplied — without it the gates cannot be exercised locally against anything but 'admin'. The generated API reference now tags each tool read/write/admin, so the classes the gates act on are visible where the tools are documented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lib/claim-gate.js and its tests stay byte-identical to the upstream copy, which is what keeps porting between the two repos risk-free. 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.
Ports the access-gate work that
node-red-contrib-mcp-servergained after it was extracted fromhal2, and adds a read/write split for hal2's own built-in tool catalog. Until now a token that
could read device state could also switch every light in the house — the only per-tool distinction
that existed was
MCP_ADMIN_TOOL_NAMES.Access model
One claim name on the Event handler carries three comma-separated any-of value lists,
combined with AND:
get_all_states,get_state,get_history,get_scenes,get_presence,get_alerts,analyze_patternsset_light,control_device,control_fan,control_cover,control_spa,control_climate,activate_sceneget_flow,deploy_flowadminThe read list is the floor; writes are checked on top of it, so
read = family/write = opslets the household see everything while only ops can change anything. All new fields default to
empty, which is no constraint, so an existing install behaves exactly as before.
Tools a caller cannot use are absent from
tools/listand from theinitializeblurb rather thanadvertised and then refused. A direct call to a hidden tool returns an MCP tool result with
isError: trueand a readable reason, so the model is told why instead of getting a generic"tool execution failed".
Three findings that shaped it
control_lightis an undocumented alias ofset_lightthat the dispatcher accepts but thecatalog does not list. A write gate keyed on a name set has to include it or it is a clean
bypass. Pinned by its own test.
get_scenesandget_alertslivein
dispatchControlToolsbut only observe. The classification is an explicit set.fails closed instead of slipping through ungated.
Scope decision: MCP only
Gates are enforced on the MCP surface, where claims are cryptographically verified.
hal2Apireads
msg.claimsstraight off the message, which any flow can set, so gating it would bedecoration rather than a boundary; it keeps its own local Allow admin tools checkbox. The gate
therefore travels in
optsthroughnode.callTool— the single choke point both surfaces reach —rather than being derived from the claims.
Ported verbatim
lib/claim-gate.jsandlib/mcp-rpc.jsare byte-identical to their upstream copies, which iswhat keeps future fixes a
cpaway in either direction. The standalonehal2MCPServerroute isrewired onto
handleRpc, replacing ~90 lines of inline dispatch. The embedded/mcproute keepsits own dispatch deliberately: its static, hardware-filtered catalog does not fit
handleRpc'sregistry shape, and generalizing it would end the byte-identical portability.
claimSatisfiedis deleted now that both callers usecreateToolGate.Nested claim paths: a dotted claim name such as
realm_access.rolesis walked as a path whenno literal key of that name exists, so Keycloak-style nested roles work. A literal key always
wins, so PocketID's flat
groupsis untouched.Hardening
Object.create(null)for the tool registry and pending-call map. Caller-supplied names like__proto__previously resolved through the prototype chain, past the "Unknown tool" check, intoa 30-second hang.
unregisterMCPTool.strips its sibling's route.
localDebugGroups—mcp-authalready accepted it but nothing supplied it, so thedebug token was permanently
adminand the gates could not be exercised locally.Editor and docs
The claim field moved out of the admin section, which is hidden whenever admin tools are off — the
read/write gates need the same claim.
hal2MCPIngained a per-tool list with a live tip that readsthe selected server's actual claim name.
docs/API.mdnow tags every tool read/write/admin.Testing
183 passing, up from 175 — including 19 borrowed claim-gate tests, 21 borrowed RPC-dispatcher
tests with the prototype-chain regression, and 10 new gate-policy tests covering the
control_lightalias, unknown-tool-is-write, and empty defaults reaching everything.Verified live against the running instance: routes register cleanly, the embedded endpoint and the
standalone server both answer real OAuth-authenticated calls after the rewire, and a read-only
token is correctly refused write tools while keeping
get_scenes/get_alerts.