ha-mcp-readonly

Read-only MCP (Model Context Protocol) server for Home Assistant. Gives AI assistants (Claude Desktop, LibreChat, Cline) full observability into your smart home — entity states, automations, scripts, devices, logs, diagnostics — without any write access. Also generates static AI context snapshots for RAG systems, ChatGPT Projects, Qwen, and other tools that accept custom knowledge files. Built in Python, runs anywhere — locally, in Docker, or as an MCP integration.

Documentation

HA-MCP-Readonly

CI Docker Python 3.11+ License: MIT

Read-only MCP (Model Context Protocol) server for Home Assistant. Gives AI assistants (Claude Desktop, LibreChat, Cline) full observability into your smart home — entity states, automations, scripts, devices, logs, diagnostics — without any write access. Also generates static AI context snapshots for RAG systems, ChatGPT Projects, Qwen, and other tools that accept custom knowledge files. Built in Python, runs anywhere — locally, in Docker, or as an MCP integration.

Requirements

  • Python 3.11+ (for local use) or Docker
  • A Home Assistant instance with a long-lived access token
    • Create one in your HA profile: Settings → Security → Long-Lived Access Tokens
  • Access to your Home Assistant config directory (for filesystem tools)

Quick Start

1. Configure environment

cp .env.example .env

Edit .env with your credentials:

HA_URL=http://your-ha-ip:8123
HA_TOKEN=your_long_lived_access_token_here
# HA_CONFIG_PATH=/config                # optional, default shown
# MCP_DEV_TOOLS_ENABLED=1               # optional, default shown
# HEALTH_CHECK_PORT=9091             # optional, default shown
# MCP_PORT=9092                       # Streamable HTTP port when enabled
# REST_API_PORT=9093                 # optional, default shown
# RUN_TESTS_ON_STARTUP=0             # optional, default shown
# OUTPUT_PATH=/app/output/ha-ai-context.md  # optional, default shown

IMPORTANT: The .env file contains your access token. It is gitignored and must never be committed.

2. Run with Docker

First, configure your credentials. Either use a .env file (recommended) or pass variables directly.

Option A — with .env file and docker compose:

cp .env.example .env
# edit .env with your HA_URL and HA_TOKEN
docker compose up -d

The included docker-compose.yml pulls the image from GitHub Container Registry and mounts your HA config read-only:

services:
  ha-mcp-readonly:
    image: ghcr.io/paulomac1000/ha-mcp-readonly:latest
    container_name: ha-mcp-readonly
    env_file: .env
    environment:
      MCP_TRANSPORT: http
      MCP_BIND_HOST: 0.0.0.0
      MCP_AUTH_TOKEN: ${MCP_AUTH_TOKEN:?Set a strong MCP_AUTH_TOKEN}
    ports:
      - "127.0.0.1:9091:9091"  # health
      - "127.0.0.1:9092:9092"  # authenticated Streamable HTTP MCP
    volumes:
      - /path/to/ha/config:/config:ro  # Replace with your HA config path (e.g., /config, ~/.homeassistant)
    tmpfs:
      - /app/output:size=256m,mode=0750,uid=10001,gid=10001
    restart: unless-stopped
    read_only: true
    cap_drop: ["ALL"]
    security_opt: ["no-new-privileges:true"]

Option B — with plain docker run:

docker run -d \
  --name ha-mcp-readonly \
  -p 127.0.0.1:9091:9091 \
  -p 127.0.0.1:9092:9092 \
  -e HA_URL=http://your-ha-ip:8123 \
  -e HA_TOKEN=your_token \
  -e MCP_TRANSPORT=http \
  -e MCP_BIND_HOST=0.0.0.0 \
  -e MCP_AUTH_TOKEN=replace-with-a-high-entropy-caller-token \
  -v /path/to/ha/config:/config:ro \
  ghcr.io/paulomac1000/ha-mcp-readonly:latest

Building locally:

docker build -t ha-mcp-readonly .
docker compose -f docker-compose.build.yml up -d

3. Run locally (Python 3.11+)

pip install -r requirements.txt
HA_URL=http://localhost:8123 HA_TOKEN=your_token python server.py

Ports

PortProtocolPurposeEndpoint
9091HTTPHealth checkGET /health
9092HTTPAuthenticated Streamable HTTP MCP/mcp
9093HTTPOptional authenticated REST API + Context Generator/api/*

Verify

# Health check
curl http://localhost:9091/health

# Readiness
curl http://localhost:9091/ready

# MCP uses an official Streamable HTTP client at http://127.0.0.1:9092/mcp.
# REST/context routes on 9093 exist only when REST_API_ENABLED=1 and require a bearer token.

Available Tools (158 with dev tools, 145 without)

Tools are organized by category (75 shown in table below). All are read-only — no state changes, no service calls, no modifications.

CategoryKey tools
Statesget_entity_state, get_states_grouped, search_entities, get_domains_summary, get_system_overview
Automationslist_automations, get_automation_code, get_automation_file_location, diagnose_automation, search_automations_by_entity, get_automation_conflicts, get_automation_entity_id
Scripts & Sceneslist_scripts, get_script_code, list_scenes, get_scene_code
Blueprintslist_blueprints, get_blueprint_code, get_blueprint_instances, get_blueprint_usage_summary, resolve_blueprint_automation
Devices & Areasget_device_details, search_devices, get_devices_by_area, get_area_devices_summary
Config entriesget_config_entry_details, search_config_entries, diagnose_config_entry, list_config_entry_domains
Integrationsget_integration_entities, get_integration_summary
Diagnosticsdiagnose_system_health, get_unavailable_entities_grouped, get_integration_health, diagnose_person_tracking
Logsget_log_insights, analyze_log_errors, get_startup_errors, get_log_timeline, search_logs
Historyget_entity_state_history_summary, get_recent_state_changes
Contextentity_get_context_tree, get_entity_dependencies, get_entity_consumers, get_context_chain
Configget_main_configuration, search_in_config, validate_yaml_syntax, read_config_file
Storagesearch_registries_batch, get_entity_registry, get_device_registry, get_area_registry, get_template_entity_code, get_cache_stats
Lovelaceget_lovelace_dashboards, get_lovelace_config, get_lovelace_resources, search_lovelace_config, get_lovelace_config_summary, diagnose_lovelace_setup
Batchbulk_search_entities, compare_entities_state, validate_yaml_batch, get_automation_codes_batch
Compositeinvestigate_entity, get_area_diagnostic, get_entity_with_automations, audit_config_orphans
Graphgraph_build_index, graph_find_references, graph_entity_impact, graph_get_neighbors, graph_detect_ghost_references, graph_detect_orphans, graph_export_mermaid
Dev toolstest_template, compare_templates, diagnose_entity, check_entity_exists, validate_automation_trigger, diagnose_template

Full tool catalog with schemas available at GET /api/tools

What's New in v2.0.0

  • Breaking — transport cleanup: Removed the legacy two-endpoint HTTP+SSE transport. The supported MCP transports are now stdio and Streamable HTTP only; MCP_TRANSPORT=sse is rejected.
  • Hardened network deployment: Explicit ASGI application with bounded request/header sizes, trusted Host policy (MCP_ALLOWED_HOSTS), exact-origin CORS, connection limits, and stateless/stateful mode selection.
  • Recursive credential redaction: All tool responses are sanitized at the operation boundary — bearer tokens, JWTs, API keys, passwords, and IP addresses are redacted from both response payloads and log output.
  • Capability discovery: Public discovery now separates supported and active transports/components and reports server, SDK, protocol, and deployment-profile identity.
  • Reliability fixes: diagnose_automation_aliases performance fix (>120s to ~4.5s on a 131-automation instance); backend health probe retries at startup with background reconciliation; blocking storage coroutines run through the bounded invocation executor.
  • Verification and evidence: ai-skills alignment uses a pinned validator and recorded real-Home-Assistant cassette tests. Exact-head hosted CI verifies 1,234 unit tests, 17 protocol tests, the installed wheel, and amd64/arm64 container artifacts. Revision-bound live-HA evidence is documented separately in docs/evidence-live-27d32c9b.md and applies only to the revision named there.

Also carried forward from v1.6.0

  • 5 tools: get_context_chain, resolve_blueprint_automation, get_cache_stats, compare_templates, get_automation_entity_id
  • Registry pagination (limit/offset on get_entity_registry, get_device_registry, get_area_registry, get_config_entries)
  • data_quality field on composite diagnostic tools
  • choose_analysis in diagnose_automation (detail_level="full")

Client configuration

Local stdio

Install the wheel and configure the client to start the server as a subprocess. This is the default transport and does not expose an MCP network port.

{
  "mcpServers": {
    "ha-mcp-readonly": {
      "command": "ha-mcp-readonly",
      "env": {
        "HA_URL": "http://homeassistant.local:8123",
        "HA_TOKEN": "replace-with-a-long-lived-access-token",
        "HA_CONFIG_PATH": "/path/to/home-assistant/config"
      }
    }
  }
}

Authenticated Streamable HTTP

Set MCP_TRANSPORT=http, MCP_AUTH_TOKEN, and a controlled bind address. The endpoint is /mcp. Legacy /sse support has been removed and MCP_TRANSPORT=sse is rejected.

{
  "mcpServers": {
    "ha-mcp-readonly": {
      "url": "http://127.0.0.1:9092/mcp",
      "headers": {
        "Authorization": "Bearer replace-with-a-high-entropy-caller-token"
      }
    }
  }
}

The default catalog contains 145 read-only tools. Developer-only tools remain disabled unless MCP_DEV_TOOLS_ENABLED=1 is set.

Context Generator

The context generator creates a bounded Markdown snapshot for offline analysis, retrieval systems, AI project knowledge, audits, and troubleshooting.

ModeData access
offlineLocal Home Assistant configuration and safe storage records only. Network access is disabled by construction.
onlineHome Assistant REST and WebSocket APIs. Missing required network access fails the run.
hybridLocal sources plus every supported API source available to the configured token.

The artifact includes the normal analysis sections and a Source Provenance and Completeness matrix. Every attempted source records its method, status, record count, byte count, redaction count, requested window, and failure or omission reason. The Comprehensive Safe Data Snapshot includes all supported, accessible data within configured bounds:

  • states, services, components, events, configuration, history, logbook, error log, calendars, and calendar events;
  • entity, device, area, floor, label, category, configuration-entry, energy, panel, Lovelace-resource, repair, system-health, and Assist-pipeline data exposed by Home Assistant;
  • to-do items and every advertised weather forecast type discovered dynamically from entity states;
  • all discoverable safe .storage records plus YAML, JSON, and file inventory data under the configured root;
  • automation, script, scene, blueprint, template, helper, person, zone, energy, HACS, cache, dependency, dashboard, and diagnostic analysis.

Credential stores are excluded. Sensitive fields, bearer tokens, JWTs, secret query parameters, and !secret values are redacted. Binary media, camera streams, backup contents, databases, and credential-bearing records are not copied. Sources unavailable because of permissions, missing integrations, unsupported commands, configured windows, or size limits remain visible in provenance rather than being silently omitted.

Relevant limits are HA_CONTEXT_HISTORY_HOURS, HA_CONTEXT_LOG_HOURS, HA_CONTEXT_CALENDAR_DAYS, HA_CONTEXT_MAX_SOURCE_BYTES, and HA_CONTEXT_MAX_OUTPUT_BYTES.

Budget-aware generation

The generator can bound output at the source instead of emitting everything and letting consumers slim it down. Options are available through the environment (HA_CONTEXT_PROFILE, HA_CONTEXT_SECTIONS, HA_CONTEXT_DETAIL, HA_CONTEXT_INCLUDE_FILES, HA_CONTEXT_INCLUDE_STORAGE, HA_CONTEXT_ON_BUDGET_EXCEEDED) and through the REST generate call:

OptionValuesDefaultEffect
profilefull, agent, compactfullSection preset. full renders everything (historical behavior); agent drops the heavy raw snapshot, log, and recent-changes sections; compact renders summary, provenance, system health, topology, and quick reference only.
maxBytesinteger ≥ 1024, ≤ 128 MiBHA_CONTEXT_MAX_OUTPUT_BYTES (96 MiB when unset)Output byte budget. Omitting maxBytes inherits the configured environment value. Sections that do not fit are omitted whole and reported.
sectionssection keys or aliasesprofile defaultExplicit selection overriding the profile. Aliases: runtime, health, provenance, logs. The executive summary and source provenance sections are always included.
repositoryFiles / include_filesbooleantrueWhen false, the comprehensive raw snapshot does not collect or serialize config file bodies. Structured analysis sections (registries, automations, devices) remain derived-metadata views and are not affected.
storageRecordsbooleantrueWhen false, the comprehensive raw snapshot does not collect safe .storage records.
detailfull, compactfullAlias that resolves to profile=compact when no explicit profile is given.
onBudgetExceededauto, fail, truncateautoResolved policy: auto behaves as fail for the full profile (preserving the historical fail-closed run) and as truncate for agent/compact; explicit fail/truncate always win.

REST option defaults are resolved by the REST layer as shown; they do not inherit their non-boolean defaults from the corresponding environment variables — only maxBytes falls back to its environment value.

The generation result reports output_bytes, uncompressed_bytes, output_sha256, profile, requested_sections (the resolved original selection), selected_sections (the effective selection including the mandatory floor), rendered_sections, omitted_sections (with exact per-section byte sizes and reasons), and truncated. Disabling repository files or storage records records an explicit policy: skip in the provenance matrix — nothing is silently omitted. The executive summary and source provenance sections are always rendered, including under explicit section selections, so every artifact carries its completeness record.

Source and runtime identity are carried by dedicated result fields: config_path identifies the audited Home Assistant configuration root (source identity), while mode, profile_revision, and generated_at identify the generating runtime conditions (runtime identity); output_sha256 pins the exact artifact bytes. Consumers can therefore always attribute an artifact to the instance state and generator revision that produced it.

Empty instances are a success case: a generation over a valid but zero-record instance completes normally, renders zero-count sections, and reports zeroed counters with truncated: false. Only invalid options, budget violations under the fail policy, or unavailable required data fail the run.

Two deliberate design decisions, recorded for issue #33 consumers: agent is the bounded operational view intended for agent consumption, while the API default remains the historical full profile for backward compatibility — callers opt into the bounded view explicitly. Truncation is applied at whole-section granularity and reported explicitly; partial (mid-section) content truncation is never applied silently, so the contract maps as included_sectionsrendered_sections, truncated_sectionsomitted_sections, and bytesoutput_bytes.

Security note: the examples above are loopback-only. When the REST adapter is exposed through a remote reverse proxy, terminate TLS at the proxy and require HTTPS from clients — plain HTTP transmits the bearer token in cleartext.

curl -X POST http://127.0.0.1:9093/api/context/generate \
  -H "Authorization: Bearer $REST_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode":"hybrid","profile":"agent","maxBytes":2097152}'

curl -X POST http://127.0.0.1:9093/api/context/generate \
  -H "Authorization: Bearer $REST_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode":"offline","detail":"compact"}'
curl -H "Authorization: Bearer $REST_API_TOKEN" \
  http://127.0.0.1:9093/api/context/status

curl -H "Authorization: Bearer $REST_API_TOKEN" \
  http://127.0.0.1:9093/api/context/download > ha-ai-context.md

REST API

The optional REST compatibility adapter is disabled by default. When enabled, every route except health requires a bearer token and uses the same manifest, capability, deadline, concurrency, response-size, and error policy as MCP.

curl -H "Authorization: Bearer $REST_API_TOKEN" \
  'http://127.0.0.1:9093/api/tools?detail=full'

curl -X POST http://127.0.0.1:9093/api/tools/get_entity_state \
  -H "Authorization: Bearer $REST_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"entity_id":"sun.sun"}'

curl -H "Authorization: Bearer $REST_API_TOKEN" \
  http://127.0.0.1:9093/api/openapi.json

Development

Setup

git clone https://github.com/paulomac1000/ha-mcp-readonly.git
cd ha-mcp-readonly
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Run tests

# Deterministic local gates
pytest tests/unit/ -q
pytest tests/protocol/ -q

# Backend-dependent suites; require an isolated Home Assistant and credentials
export HA_URL=http://your-ha:8123
export HA_TOKEN=your_token
pytest tests/smoke/ tests/integration/ tests/e2e/ -q

Backend-dependent tests report skips when the required Home Assistant environment is absent. CI installs the wheel in a clean environment, executes a real stdio subprocess, and verifies health, REST metadata, the offline context lifecycle, and authenticated Streamable HTTP against built release containers. The release workflow separately builds one multi-platform candidate into quarantine, smoke-tests its exact digest on amd64 and arm64, and promotes only that digest from the protected publisher.

Lint & format

ruff check .
ruff format --check .

Architecture

server.py                  # Main entry point — FastMCP + REST API + health check
context_generator/
├── config.py              # Immutable per-run configuration
├── constants.py           # Legacy/static analyzer defaults and HA YAML loader
├── runtime.py             # Context-local runtime and provenance scope
├── provenance.py          # Completeness matrix and redaction
├── snapshot.py            # Safe filesystem, REST, and WebSocket collectors
├── storage_policy.py      # Positive allowlist for model-visible .storage data
├── core.py                # Isolated generation entry points
├── analyzers.py           # Domain analyzers
├── formatters.py          # Atomic bounded Markdown output
└── utils.py               # Runtime-aware registry and API adapters

ha_graph/
└── graph_builder.py       # HA Semantic Graph: build, query, and export

tools/
├── automations.py         # Automation analysis (17 tools)
├── batch_operations.py    # Bulk entity operations (5 tools)
├── blueprints.py          # Blueprint management (4 tools)
├── capabilities.py        # Zero-I/O MCP introspection tool catalog (1 tool)
├── categories.py          # Category management (automation, script, scene, helpers) (1 tool)
├── composite.py           # Composite diagnostic tools (4 tools)
├── config.py              # Configuration file tools (10 tools)
├── config_entries.py      # Config entry diagnostics (4 tools)
├── devices.py, areas.py   # Device and area tools (6+1 tools)
├── dev_tools.py           # Template testing, validation (13 tools)
├── diagnostics.py         # System health, energy dashboard (18 tools)
├── entity_context.py      # Entity context tree (2 tools)
├── entity_dependencies.py # Entity dependency graph (2 tools)
├── filesystem_explorer.py # Secured filesystem browsing (3 tools)
├── graph_tools.py           # HA entity graph tools (7 tools)
├── health_reporter.py     # Health score and metrics (1 tool)
├── helpers_health.py      # Helper entity health diagnostics (1 tool)
├── history.py             # State history and recent changes (2 tools)
├── integrations.py        # Integration entity analysis (2 tools)
├── logs.py                # Log analysis and insights (8 tools)
├── manifests.py           # TOOL_MANIFESTS, risk prefix injection
├── observability.py       # request_id, invocation counters
├── scripts.py, scenes.py  # Script and scene inspection (2+2 tools)
├── states.py              # Entity state queries (12 tools)
├── storage.py             # Registry dump and search tools (30 tools)
├── utils.py               # Shared: HA API client, registry loader, log sanitizer
└── yaml_utils.py          # HomeAssistantLoader for HA-specific YAML tags

tests/
├── unit/                  # 39 test files, 1181 tests, fully mocked
├── integration/           # Real HA tests (requires HA_URL + HA_TOKEN)
├── smoke/                 # REST API smoke tests (requires local server)
└── e2e/                   # End-to-end pipeline tests (requires real HA)

Security

  • Read-only by design — no write operations to Home Assistant. Cannot modify states, execute services, or trigger automations.
  • Filesystem restrictions — access limited to /config directory. Path traversal (.., ~) blocked. Max file size 10MB. Max directory depth 20.
  • Auth data blockedauth, auth_provider.*, onboarding registries are never returned.
  • Credential redactionHA_TOKEN is never logged or exposed in outputs. JWTs, passwords, API keys, and IP addresses are sanitized from log output.

Notes

  • The server may expose 9091 (health), 9092 (authenticated Streamable HTTP MCP), and 9093 (optional authenticated REST/context adapter). Stdio remains the default MCP transport.
  • MCP_DEV_TOOLS_ENABLED=0 disables template execution and debugging tools for production use.
  • Security note: Ports 9091-9093 should not be exposed publicly. Use firewall rules or reverse proxy with authentication if needed.
  • Registry files (areas, devices, entities, config entries) are cached for 5 minutes to reduce filesystem I/O.
  • All tool responses return JSON with a success field — always check this before reading data.

Troubleshooting

For common issues and solutions, see docs/documentation.md#troubleshooting.

License

MIT — see LICENSE for details.