Configuration

Authentication

JWT, the token manager, expired-token lockout, and how authentication differs per entry point.


The custom HTTP entry point (src/index.ts) layers a session-token system on top of the JSON-RPC interface. STDIO and fastmcp HTTP skip this — they trust the local process or the network boundary, respectively. This page is about the custom stack.

Token lifecycle

  1. Client connects. A new request to /mcp without a token is rejected with 401 Unauthorized.
  2. Login. The client posts HASS_TOKEN to /api/auth/login (over HTTPS in production). The server verifies the token by opening a WebSocket to HA and trying to authenticate with it. If HA accepts, the server mints a JWT (HS256 with JWT_SECRET) that expires in 24 hours.
  3. Bearer. Subsequent requests carry Authorization: Bearer <jwt>. The middleware verifies the signature, checks the expiry, and attaches the user info to the request context.
  4. Refresh. When a JWT is within an hour of expiry, the response includes a refreshed token in the X-Refresh-Token header. The client should pick it up and start using it for the next request.
  5. Logout. The client deletes its token. The server has no session table to clean up — JWT is stateless.

The token manager

src/security/TokenManager.ts is the in-memory store for active tokens and the lockout state. It holds:

  • The set of revoked JWTs (so a logout propagates across instances… well, across the one instance — see below).
  • The set of HASS_TOKEN strings that have been seen and the last time they were used.
  • The failed-login counter per IP.

A failed login (HA rejects the token, or the WebSocket fails to open) increments the counter. After 5 failed attempts from the same IP in a 15-minute window, the IP is locked out for 15 minutes. The lockout is in-memory and resets on process restart, which is fine — its purpose is to slow down brute-force token guessing, not to be a security boundary.

Expired-token lockout

If a request arrives with an expired or tampered token, the middleware returns 401 Unauthorized with a WWW-Authenticate: Bearer error="invalid_token" header. The client should re-login. The server does not automatically retry with a different token, because there is no different token to retry with — JWT is stateless.

Per-entry-point differences

Entry pointAuthWhy
src/index.ts (custom HTTP)JWT, login endpoint, rate limit per IP, lockoutThe “production” stack; assumes untrusted network.
src/stdio-server.ts (fastmcp STDIO)None beyond the local process boundaryThe client is a local editor with no remote surface.
src/http-server.ts (fastmcp HTTP)None (or whatever the fastmcp SDK adds)Useful for trusted-network or behind-reverse-proxy deploys.

If you put the custom HTTP entry on the public internet, terminate TLS at a reverse proxy (Caddy, nginx, Cloudflare Tunnel, …) and put an additional auth layer in front — JWT alone is not enough.

CSRF

The custom stack uses bearer tokens in the Authorization header, so CSRF is not a concern (no cookies). If you add cookie-based auth in the future, add a CSRF token and an Origin check.

Helmet + sanitize-html

The middleware chain (src/security/enhanced-middleware.ts) also applies:

  • helmet with a strict CSP. The default policy allows inline scripts for the docs site’s theme init; in production, tighten it.
  • sanitize-html on every text body before it’s handed to a tool. This prevents an attacker from smuggling HTML into a description field that a downstream UI might render.

Generating a JWT_SECRET

In production, generate a fresh secret and store it as a secret in your platform of choice (GitHub Actions secret, Docker secret, k8s Secret, Vault, …). Never reuse the example value from .env.example.

openssl rand -base64 48

Or with Bun:

bun -e 'console.log(crypto.randomUUID() + crypto.randomUUID() + crypto.randomUUID())'

The schema enforces min(32) so the bare minimum is 32 characters, but 48+ gives you margin to rotate.

Next

  • Tools — how individual tools can declare read-only or destructive hints.
  • Deployment > HTTP+WS — putting this behind a reverse proxy in production.