Nine roles reached their MCP surface as the deployment administrator or with
an account password kept beside the account in credentials. A Moodle site
admin bypasses every capability check, a Nextcloud app password is not
scopable, a Gitea token reads every private repository, and a GitLab token on
root reads every private project. Each role now declares its own principal in
meta/users.yml and provisions it, so the exposed surface is what the service
role permits. jellyfin, zammad and snipe-it kept their account but took its
password from a credential named token; they read the registry too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role declared a server-direction MCP service but never provisioned one, and the declared endpoint pointed at /mcp_server/sse, the legacy path. Home Assistant serves MCP under /api/mcp, and its mcp_server integration has no YAML schema, so the config entry only exists through the config-entries flow API.
The provisioning script drives that path over the container loopback: onboard the administrator when the hub is fresh (falling back to the login flow when it is not), mint a long-lived token over the websocket API, then create the mcp_server config entry with it. The token is persisted through sys-token-store, which is why meta/users.yml registers the key; a second role registering a token for the same user only works since the per-key user merge.
The whole path is skipped when a token already exists, so a redeploy costs one lookup. Handlers flush before it runs because the hub container has to exist before anything talks to its API.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>