Architecture Overview
The high-level layers of the server, how a request flows through them, and the key files.
The server is structured as four loosely-coupled layers that sit between an MCP client (Claude, Cursor, anything else) and your Home Assistant instance. The boundaries are drawn so each layer can be swapped or extended without touching the others.
The layers
┌────────────────────────────────────────────────────────────────┐
│ Transport (HTTP+WS via custom MCPServer, or fastmcp STDIO, │
│ or fastmcp HTTP) │
├────────────────────────────────────────────────────────────────┤
│ Middleware (validation, error handling, JWT, rate limit, │
│ helmet, sanitize-html) │
├────────────────────────────────────────────────────────────────┤
│ Tool (BaseTool subclass with a Zod schema + an │
│ execute() that talks to the HA client) │
├────────────────────────────────────────────────────────────────┤
│ HA Client (WebSocket connection to /api/websocket on the HA │
│ instance; command queue + event subscription) │
└────────────────────────────────────────────────────────────────┘
A request flows top-to-bottom: the transport parses a JSON-RPC envelope and hands it to the middleware chain, which validates, authenticates, and rate-limits it. The middlewares then dispatch to the named tool, which validates its own arguments against a Zod schema and runs execute(). The tool talks to the HA client, which sends a command over the WebSocket and waits for the response. The result flows back up the same path as a JSON-RPC reply.
Key files
| Concern | Path | Notes |
|---|---|---|
| HTTP+WS server | src/index.ts | Wraps MCPServer in Express; serves /mcp and /mcp/ws. |
| STDIO server | src/stdio-server.ts | Uses fastmcp v3 over stdin/stdout. |
| HTTP server | src/http-server.ts | Uses fastmcp v3 over HTTP. |
| MCP core | src/mcp/MCPServer.ts | Singleton, manages tool registry + middlewares. |
| Transports | src/mcp/transports/ | http.transport.ts, stdio.transport.ts. |
| Tool base class | src/mcp/BaseTool.ts | Generic <TInput, TOutput> over Zod schemas. |
| HA client | src/hass/ | WebSocket API client + state cache. |
| Tools | src/tools/homeassistant/ | Domain-specific tools (lights, climate, …). |
| Generic tools | src/tools/ | Cross-cutting helpers (history, search, etc.). |
| Config | src/config/app.config.ts | Zod-validated env loader (single source of truth). |
| Security | src/security/ | JWT, rate limit, helmet, sanitize-html. |
| SSE | src/sse/ | Event streaming subsystem. |
Build outputs
The three entry points compile to three different files via esbuild (see package.json scripts):
| Entry | Output | Module |
|---|---|---|
src/index.ts | dist/index.cjs | CommonJS |
src/stdio-server.ts | dist/stdio-server.mjs | ESM |
src/http-server.ts | dist/http-server.mjs | ESM |
External deps (@modelcontextprotocol/sdk, fastmcp, zod, winston, …) are not bundled — they’re added to the esbuild --external flag and resolved at runtime from node_modules. This keeps the bundles small and makes the production build predictable on plain Node.
Why three entry points?
Each target client has different needs:
- STDIO is what Claude Desktop, Cursor, and most local editor integrations speak. It’s the simplest, most firewall-friendly option.
- HTTP+WS is what hosted AI services and web UIs need. The custom
MCPServer+ Express stack gives us full control over the request lifecycle, rate limiting per IP, and aWebSocket /mcp/wsendpoint for streaming events. - HTTP (fastmcp) is the same shape as HTTP+WS but uses the
fastmcpv3 SDK, which is a thinner wrapper. It’s useful when you want a quick remote deploy without any of the bespoke middleware.
In practice you usually only need one. See Deployment to pick the right one for your setup.
Next
- Entry Points — deep dive on each of the three.
- Tool System — how tools are defined, registered, and dispatched.
- HA Client — the WebSocket layer under everything.