Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 2026.10.5.2 — 2026-10-05

- Expose action tools with readable ASCII names derived from their configured names, such as `otevrit_branu`, while retaining their original titles and descriptions.
- Keep legacy `action_<id>` calls compatible with cached client tool lists.
- Prevent collisions with the status tool, legacy identifiers, literal names and generated suffixes; every configured action remains available.
- Bind retry results to the stable action ID, so switching between readable and legacy aliases cannot execute the same request twice.
- Keep fixed empty tool arguments, OAuth permissions and private diagnostic categories unchanged.

Updating with the same configured origin and permissions preserves configuration and OAuth grants. Clients with cached tool lists may display old names until their tool list is refreshed. Renaming a configured action still revokes access and requires new pairing. This release passed 89 Python tests, 5 JavaScript tests and package checks; live client/display behavior needs separate acceptance testing.

## 2026.10.5.1 — 2026-10-05

- Support public HTTPS addresses beyond Nabu Casa: your own domain or reverse proxy, including nonstandard ports.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ This is an independent integration, not the official Home Assistant MCP Server o
Install the published release through HACS:

1. In HACS, open **Custom repositories** and add `https://github.com/Jenik5/GrokConnectorForHomeAssistant` as an **Integration**.
2. Download **2026.10.5.1** or a later stable release. Stable releases do not require beta versions to be enabled in HACS.
2. Download **2026.10.5.2** or a later stable release. Stable releases do not require beta versions to be enabled in HACS.
3. Restart Home Assistant to load the newly installed Python integration.
4. Open **Settings → Devices & services → Add integration → Grok Connector**.
5. Enter your public HTTPS origin, for example `https://your-instance.ui.nabu.casa` or `https://ha.example.org:8125`, and choose the MCP language. Enter only the origin, without credentials, a path, query or fragment.
Expand All @@ -52,6 +52,8 @@ HACS manages this repository as a custom repository; inclusion in the HACS defau

Each action has a **name**, an optional **icon**, a **description for Grok** and an HA **action sequence**. Choose the icon through HA's native icon picker; actions without an icon use the play symbol. The visual editor supports service actions, device actions, scripts, automations, scenes, conditions and sequences. The configuration is validated by HA before it becomes an MCP tool.

From **2026.10.5.2**, action tool names are readable ASCII forms of the configured name: `Otevřít bránu` becomes `otevrit_branu`. Diacritics are normalized and the base name is limited to 48 characters. Empty, duplicate or reserved names gain a short ID suffix and, when needed, a counter. Names are unique across the status tool, every action and legacy aliases. The original title and description are retained. Legacy `action_<id>` calls remain accepted for cached clients; an alias change during a retry returns the same cached result within the existing session. Cached tool lists may display old names until refreshed. Updating alone preserves pairing; renaming a configured action continues to revoke access.

| Setting | What Grok gets |
| --- | --- |
| Selected entities | Current entity ID, friendly name, state and optional unit; other attributes are not exposed |
Expand Down
2 changes: 1 addition & 1 deletion custom_components/grok_connector/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Independent paths and bounds of the community connector."""
DOMAIN = 'grok_connector'
VERSION = '2026.10.5.1'
VERSION = '2026.10.5.2'
MCP_PATH = '/api/grok_connector/mcp'
OAUTH_PATH = '/api/grok_connector/oauth'
RESOURCE_METADATA_PATH = '/.well-known/oauth-protected-resource' + MCP_PATH
Expand Down
36 changes: 31 additions & 5 deletions custom_components/grok_connector/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import asyncio
import copy
import json
import re
import secrets
import time
import unicodedata
from .const import VERSION
from .i18n import text

Expand All @@ -12,6 +14,10 @@
SESSION_LIFETIME = 1800
MAX_SESSIONS = 128

def tool_slug(name):
ascii_name = unicodedata.normalize('NFKD',name).encode('ascii','ignore').decode()
return re.sub(r'[^a-z0-9]+','_',ascii_name.lower()).strip('_')[:48]

def content(data,error=False):
return {'content':[{'type':'text','text':json.dumps(data,ensure_ascii=False)}],'isError':error}

Expand Down Expand Up @@ -71,12 +77,29 @@ def status(self):
entities.append(item)
return {'entities':entities}

def action_tools(self):
"""Readable names never shadow a status tool, literal slug or legacy alias."""
slugs = [tool_slug(action.name) for action in self.policy.actions]
literal_names = set(slugs)
names = {'entities_status', *('action_'+action.id for action in self.policy.actions)}
result = {}
for action,name in zip(self.policy.actions,slugs):
if not name or name in names or re.fullmatch(r'action_[a-f0-9]{32}',name):
stem = (name or 'action')+'_'+action.id[:6]
name,index = stem,2
while (name in names or name in literal_names
or re.fullmatch(r'action_[a-f0-9]{32}',name)):
name,index = stem+'_'+str(index),index+1
names.add(name)
result[name] = action
return result

def tools(self):
tools = [{'name':'entities_status','description':text(self.catalogs,self.language,'tool_status'),
'inputSchema':copy.deepcopy(EMPTY_INPUT),
'annotations':{'readOnlyHint':True,'destructiveHint':False,'idempotentHint':True,'openWorldHint':False}}]
for action in self.policy.actions:
tools.append({'name':'action_'+action.id,
for name,action in self.action_tools().items():
tools.append({'name':name,
'title':action.name,
'description':text(self.catalogs,self.language,'tool_action').format(name=action.name,description=action.description),
'inputSchema':copy.deepcopy(EMPTY_INPUT),
Expand Down Expand Up @@ -121,8 +144,10 @@ async def rpc(self,message,principal,*,session=None):
if session is not None and not self.session_active(session,principal):
return self.error(request_id,-32000,'MCP session expired')
actions = {'action_'+action.id:action for action in self.policy.actions}
actions.update(self.action_tools())
if name not in actions:
return self.error(request_id,-32602,'Action not permitted')
action_id = actions[name].id
now = self.clock()
self.replies = {key:value for key,value in self.replies.items() if value[0] > now}
# JSON-RPC IDs identify requests within an MCP session, not
Expand All @@ -131,12 +156,12 @@ async def rpc(self,message,principal,*,session=None):
key = (principal,session,type(request_id),request_id)
if session is not None and key in self.replies:
_,previous,cached = self.replies[key]
if previous != name:
if previous != action_id:
return self.error(request_id,-32602,'Request ID already used for another action')
result = copy.deepcopy(cached)
else:
try:
await self.action_runner(actions[name].id)
await self.action_runner(action_id)
except Exception:
result = content({'error':'action_execution_uncertain',
'instruction':'Do not retry automatically. Check the state and HA trace.'},True)
Expand All @@ -145,7 +170,8 @@ async def rpc(self,message,principal,*,session=None):
if session is not None and session in self.sessions:
if len(self.replies) >= 256:
self.replies.pop(next(iter(self.replies)))
self.replies[key] = (self.clock()+120,name,copy.deepcopy(result))
# Both the readable name and legacy alias identify this action.
self.replies[key] = (self.clock()+120,action_id,copy.deepcopy(result))
else:
return self.error(request_id,-32601,'Method not found')
return {'jsonrpc':'2.0','id':request_id,'result':result}
Expand Down
3 changes: 2 additions & 1 deletion custom_components/grok_connector/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@ async def post(self, request):
self.diagnostic(request, "mcp", "request", bearer_valid=True,
rpc_method=category("rpc_method", message.get("method")),
tool=category("tool", "configured_action" if isinstance(params, dict)
and isinstance(params.get("name"), str) and re.fullmatch(r"action_[a-f0-9]{32}", params["name"])
and isinstance(params.get("name"), str) and (params["name"] in self.runtime.gateway.action_tools()
or re.fullmatch(r"action_[a-f0-9]{32}", params["name"]))
else params.get("name") if isinstance(params, dict) else None))
task = self.hass.async_create_task(self.runtime.gateway.rpc(message, principal, session=session), "Grok connector request")
response = await asyncio.shield(task)
Expand Down
2 changes: 1 addition & 1 deletion custom_components/grok_connector/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
"issue_tracker": "https://github.com/Jenik5/GrokConnectorForHomeAssistant/issues",
"requirements": [],
"single_config_entry": true,
"version": "2026.10.5.1"
"version": "2026.10.5.2"
}
4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The integration domain is `grok_connector`. Its MCP endpoint is `/api/grok_conne

HA's admin config/options flow owns the reading list and named action sequences. `policy.py` parses immutable JSON-backed configuration with size limits. `config_flow.py` uses HA's entity and action selectors. HA's `SCRIPT_SCHEMA` and `async_validate_actions_config` validate sequences; `Script` executes them with a new HA context and no user-supplied MCP variables.

`gateway.py` is independent of HA. It implements JSON-RPC discovery, a read-only `entities_status` tool and one `action_<stable UUID>` tool per named action. Tools have empty argument schemas and reject incoming target/service/variable overrides. Friendly action titles and descriptions are separate from their stable IDs.
`gateway.py` is independent of HA. It implements JSON-RPC discovery, a read-only `entities_status` tool and one readable ASCII tool name per configured action. The base is derived from the configured name; collision handling reserves status, all legacy `action_<stable UUID>` aliases and all literal slugs before allocating suffixes/counters. Calls through legacy aliases remain supported and cannot be shadowed by another action's display name. Tools have empty argument schemas and reject incoming target/service/variable overrides. Friendly action titles and descriptions remain unchanged; execution and retry identity use the stable action ID.

The reading list is a data-disclosure boundary, not an action-target boundary. Actions may intentionally operate on different entities or call other services, using their fixed administrator-authored sequences. Device conditions and confirmation logic remain in HA configuration. The connector contains no assumptions about garages, contact sensors or particular device domains.

Expand All @@ -26,7 +26,7 @@ During replacement/revocation, an authorization barrier rejects new HTTP authori

HA actions that have already occurred cannot be undone by revocation. An independently started script/automation may continue under its own HA lifecycle. Sequence return, condition termination and service completion are not physical-device confirmation. Execution failures produce a generic uncertainty response without exception details; the client should check state or HA traces before any deliberate retry.

Inline action sequences are serialized. JSON-RPC notifications never execute commands. Duplicate action request IDs are cached for 120 seconds (up to 256 results), scoped to the OAuth grant and MCP session; both string and numeric IDs retain their JSON types. Different sessions may use the same ID for different actions or deliberately repeat the same action. Reusing an ID for a different action in one session remains an error.
Inline action sequences are serialized. JSON-RPC notifications never execute commands. Duplicate action request IDs are cached for 120 seconds (up to 256 results), scoped to the OAuth grant and MCP session; both string and numeric IDs retain their JSON types. The cached action identity is its stable ID, so retrying through the readable or legacy alias returns the same reply. Different sessions may use the same ID for different actions or deliberately repeat the same action. Reusing an ID for a different action in one session remains an error.

Successful initialization returns a random, cryptographically secure `MCP-Session-Id` header. Sessions are bound to the authenticated OAuth grant, expire after 30 minutes of inactivity and have a hard limit of 128. Expiry, explicit DELETE or eviction drops that session's replies. An unknown, expired or foreign session header returns HTTP 404; possession of a session ID never replaces bearer authentication. Policy change, revocation and unload clear all sessions and replies. Queued actions recheck authorization and session validity before execution.

Expand Down
2 changes: 2 additions & 0 deletions docs/README.cs.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Označ entity ke čtení; jejich typ není omezen. V **Konfigurovat → Akce →

Grok může vyvolat připravenou sekvenci, nemůže jí podstrčit jiný cíl ani parametry. Podmínky a rozsah sekvence určuješ ty. Konektor zveřejní stav, název, ID a případnou jednotku vybraných entit; ostatní atributy neposílá.

Od verze `2026.10.5.2` mají nástroje akcí čitelné názvy: například `Otevřít bránu` se nabídne jako `otevrit_branu`. Shodné a rezervované názvy se bezpečně odliší příponou. Původní identifikátory zůstávají funkční; aktualizace sama nevyžaduje nové párování. Klient může zobrazovat staré názvy do obnovení seznamu nástrojů. Přejmenování akce dál vyžaduje nové párování.

## Připojení Groka

V **Konfigurovat → Párovací kód** se zobrazí přesná adresa a jednorázový kód platný 10 minut. Adresa má tvar `https://tvoje-instance.ui.nabu.casa/api/grok_connector/mcp`. V Groku vytvoř vlastní MCP konektor a kód zadej v jeho přihlašovacím okně.
Expand Down
2 changes: 2 additions & 0 deletions docs/README.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Wähle die lesbaren Entitäten. Unter **Konfigurieren → Aktionen** kannst du e

Grok darf nur die vorbereiteten Werkzeuge ohne zusätzliche Ziele oder Parameter aufrufen. Die lesbare Entitätenliste begrenzt die Datenweitergabe, nicht die Reichweite einer von dir konfigurierten Aktionsfolge.

Ab Version `2026.10.5.2` erhalten Aktionswerkzeuge lesbare Namen: `Garagentor öffnen` wird beispielsweise zu `garagentor_offnen`. Gleiche und reservierte Namen werden durch einen eindeutigen Zusatz unterschieden. Alte Kennungen bleiben gültig; das Update allein erfordert keine neue Kopplung. Ein Client kann alte Namen anzeigen, bis seine Werkzeugliste aktualisiert wird. Das Umbenennen einer Aktion erfordert weiterhin neue Kopplung.

## Grok verbinden

**Konfigurieren → Kopplungscode** zeigt die genaue MCP-Adresse und einen einmaligen Code, gültig für zehn Minuten. Die Adresse lautet `https://deine-instanz.ui.nabu.casa/api/grok_connector/mcp`. Erstelle in Grok einen benutzerdefinierten MCP-Connector und gib den Code in dessen Autorisierungsfenster ein.
Expand Down
2 changes: 2 additions & 0 deletions docs/README.pl.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Wybierz encje do odczytu. W **Konfiguruj → Akcje** dodaj nazwę, opis dla Grok

Grok może wywołać tylko przygotowane narzędzie, bez podawania innego celu lub dodatkowych parametrów. Lista encji do odczytu ogranicza ujawniane dane; zakres skonfigurowanej akcji określa jej sekwencja.

Od wersji `2026.10.5.2` narzędzia akcji mają czytelne nazwy: `Otwórz bramę` staje się na przykład `otworz_brame`. Identyczne i zastrzeżone nazwy otrzymują unikalny sufiks. Stare identyfikatory nadal działają; sama aktualizacja nie wymaga ponownego parowania. Klient może wyświetlać stare nazwy do odświeżenia listy narzędzi. Zmiana nazwy akcji nadal wymaga ponownego parowania.

## Połączenie z Grokiem

**Konfiguruj → Kod parowania** pokazuje dokładny adres MCP i jednorazowy kod ważny przez dziesięć minut. Adres ma postać `https://twoja-instancja.ui.nabu.casa/api/grok_connector/mcp`. Utwórz własny konektor MCP w Groku i wpisz kod w jego oknie autoryzacji.
Expand Down
2 changes: 2 additions & 0 deletions docs/README.sk.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Vyber entity na čítanie. V **Konfigurovať → Akcie** pridaj názov, opis pre

Grok môže vyvolať pripravený nástroj bez dodatočných cieľov alebo parametrov. Zoznam na čítanie obmedzuje zdieľané údaje; rozsah nakonfigurovanej akcie určuje jej sekvencia.

Od verzie `2026.10.5.2` majú nástroje akcií čitateľné názvy: napríklad `Otvoriť bránu` sa ponúkne ako `otvorit_branu`. Zhodné a rezervované názvy sa bezpečne odlíšia príponou. Pôvodné identifikátory zostávajú funkčné; samotná aktualizácia nevyžaduje nové párovanie. Klient môže zobrazovať staré názvy do obnovenia zoznamu nástrojov. Premenovanie akcie naďalej vyžaduje nové párovanie.

## Pripojenie Groka

**Konfigurovať → Párovací kód** ukáže presnú adresu MCP a jednorazový kód platný desať minút. Adresa má tvar `https://tvoja-instancia.ui.nabu.casa/api/grok_connector/mcp`. V Groku vytvor vlastný konektor MCP a kód zadaj v jeho autorizačnom okne.
Expand Down
8 changes: 8 additions & 0 deletions docs/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,11 @@ See [the acceptance procedure](RELEASING.md) for deliberate follow-up tests.
The custom-origin contribution and maintainer hardening passed 80 local Python tests, 5 JavaScript tests and package checks. New regressions cover ambiguous URLs, browser IPv4 representations, custom-port discovery metadata, exact Origin/resource boundaries, PKCE consent/exchange, refresh rotation and existing Nabu Casa grant compatibility. See the [security assessment](SECURITY-ASSESSMENT-2026-10-05.md).

These checks use the shipped code with framework adapters. No live HA deployment, proxy/TLS audit, physical action or Tesla test was performed for this release preparation. The contributor reported successful custom-domain pairing; that is separate from maintainer-verified acceptance.

## Readable action tool names — 2026-10-05 / 2026.10.5.2

PR [#4](https://github.com/Jenik5/GrokConnectorForHomeAssistant/pull/4) replaces opaque action names in tool discovery with ASCII names derived from configured titles. Review found that the original collision handling could drop actions or redirect a legacy alias to a different configured action. Four added regression tests failed on the original implementation, including shared six-character ID prefixes, literal/generated suffix collisions, legacy alias shadowing and alias changes during retries.

The maintainer fix reserves status and legacy names, avoids all literal slugs during suffix allocation, and rechecks generated names until unique. Retry identity uses the immutable action ID. The fixed implementation passed **89 Python tests**, **5 JavaScript tests** and package checks. Coverage includes all 64 configured actions with empty/non-ASCII/truncated/reserved names, calling each action through readable and legacy names, mixed-alias concurrent retries, authorization and argument rejection, and private HTTP diagnostic categories. These names follow the [MCP tool naming guidance](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names).

The tests use actual connector modules with framework adapters and simulated action runners. No household action, live HA update or Grok/Tesla display test was performed for this release preparation. The contributor's report of live use is separate from maintainer-verified evidence. Existing OAuth, origin/port checks, policy revocation and session-isolation regressions remain covered.
Loading
Loading