Download mcp_server/server.py from YYP0502/Bot: direct link, hf CLI and curl.
- Browser
- Download file 4.02 kB
-
https://huggingface.co/spaces/YYP0502/Bot/resolve/main/mcp_server/server.py
- Command line
-
hf download hf://spaces/YYP0502/Bot/mcp_server/server.py
-
curl -L -o server.py https://huggingface.co/spaces/YYP0502/Bot/resolve/main/mcp_server/server.py
4.02 kB
| """Authenticated Streamable HTTP bridge to Home Assistant.""" | |
| import os | |
| import secrets | |
| from typing import Any | |
| import httpx | |
| from mcp.server.fastmcp import FastMCP | |
| from mcp.server.transport_security import TransportSecuritySettings | |
| from starlette.responses import JSONResponse | |
| HA_URL = os.getenv("HA_URL", "http://127.0.0.1:8123").rstrip("/") | |
| HA_TOKEN = os.getenv("HA_TOKEN", "") | |
| MCP_API_KEY = os.getenv("MCP_API_KEY", "") | |
| mcp = FastMCP("Home Assistant", stateless_http=True, json_response=True, streamable_http_path="/mcp", | |
| transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False)) | |
| async def ha_request(method: str, path: str, data: dict | None = None) -> Any: | |
| if not HA_TOKEN: | |
| raise ValueError("Set the HA_TOKEN Space secret to a Home Assistant long-lived token.") | |
| async with httpx.AsyncClient(timeout=45, trust_env=False) as client: | |
| response = await client.request(method, HA_URL + "/api/" + path, | |
| headers={"Authorization": "Bearer " + HA_TOKEN}, json=data) | |
| if response.is_error: | |
| raise ValueError(f"Home Assistant returned HTTP {response.status_code}.") | |
| return response.json() | |
| async def list_entities(domain: str | None = None) -> list[dict]: | |
| """List actual entity IDs, states and attributes. Optionally filter by domain (light, climate, etc.).""" | |
| states = await ha_request("GET", "states") | |
| return [s for s in states if domain is None or s["entity_id"].startswith(domain + ".")] | |
| async def get_entity_state(entity_id: str) -> dict: | |
| """Read an entity's current state and attributes using its actual entity ID.""" | |
| validate_name(entity_id, dotted=True) | |
| return await ha_request("GET", "states/" + entity_id) | |
| async def list_services() -> list[dict]: | |
| """Discover available HA services and their parameters before controlling devices.""" | |
| return await ha_request("GET", "services") | |
| async def call_service(domain: str, service: str, service_data: dict[str, Any] | None = None) -> Any: | |
| """Execute a Home Assistant service. This changes devices or automations. Use actual service names and entity IDs discovered by the read tools; service_data is the HA JSON body, e.g. {"entity_id":"light.room"}. No device whitelist is applied.""" | |
| validate_name(domain) | |
| validate_name(service) | |
| return await ha_request("POST", f"services/{domain}/{service}", service_data or {}) | |
| def validate_name(value: str, dotted: bool = False): | |
| import re | |
| pattern = r"[a-z0-9_]+\.[a-z0-9_]+" if dotted else r"[a-z0-9_]+" | |
| if not re.fullmatch(pattern, value): | |
| raise ValueError("Invalid Home Assistant identifier.") | |
| async def health(request): | |
| """Authenticated routing check; does not test HA token validity or HA readiness.""" | |
| return JSONResponse({"service": "home-assistant-mcp", "transport": "streamable-http", | |
| "endpoint": "/mcp", "ha_token_configured": bool(HA_TOKEN)}) | |
| class BearerAuth: | |
| def __init__(self, app): | |
| self.app = app | |
| async def __call__(self, scope, receive, send): | |
| if scope["type"] == "http": | |
| if scope["path"] == "/mcp/": | |
| scope = {**scope, "path": "/mcp", "raw_path": b"/mcp"} | |
| if not MCP_API_KEY: | |
| response = JSONResponse({"error": "MCP is disabled until MCP_API_KEY is configured"}, status_code=503) | |
| return await response(scope, receive, send) | |
| headers = dict(scope["headers"]) | |
| actual = headers.get(b"authorization", b"") | |
| expected = ("Bearer " + MCP_API_KEY).encode() | |
| if not secrets.compare_digest(actual, expected): | |
| response = JSONResponse({"error": "Unauthorized"}, status_code=401, | |
| headers={"WWW-Authenticate": "Bearer"}) | |
| return await response(scope, receive, send) | |
| await self.app(scope, receive, send) | |
| app = BearerAuth(mcp.streamable_http_app()) | |