Skip to main content
Glama
smithery-sdk.ts10.9 kB
/** * Smithery Entry Point for Home Assistant MCP Server * * This module provides the entry point required by Smithery's TypeScript runtime. * It exports a default createServer function and configSchema following Smithery's conventions. * * @see https://smithery.ai/docs/build/deployments/typescript */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import { tools } from "./tools/index"; import { listResources, getResource } from "./mcp/resources"; import { getAllPrompts, renderPrompt } from "./mcp/prompts"; /** * Configuration schema for Smithery * All fields are optional to allow tool discovery without requiring configuration */ export const configSchema = z.object({ hassToken: z .string() .optional() .describe( "Long-lived access token for Home Assistant. Create one in your Home Assistant profile under Security > Long-lived access tokens" ), hassHost: z .string() .optional() .default("http://homeassistant.local:8123") .describe("Home Assistant server URL (e.g., http://192.168.1.100:8123)"), hassSocketUrl: z .string() .optional() .describe("Home Assistant WebSocket URL. Auto-derived from hassHost if not provided"), debug: z.boolean().optional().default(false).describe("Enable debug logging for troubleshooting connection issues"), }); /** * Tool annotations following MCP specification for trust & safety */ interface ToolAnnotations { title?: string; readOnlyHint?: boolean; destructiveHint?: boolean; openWorldHint?: boolean; } /** * Format tool name as human-readable title */ function formatToolTitle(name: string): string { return name .split("_") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); } /** * Get MCP annotations for a tool based on its behavior */ function getToolAnnotations(toolName: string, existingAnnotations?: ToolAnnotations): ToolAnnotations { // Use existing annotations if provided by the tool if (existingAnnotations) { return { title: existingAnnotations.title || formatToolTitle(toolName), readOnlyHint: existingAnnotations.readOnlyHint, destructiveHint: existingAnnotations.destructiveHint, openWorldHint: existingAnnotations.openWorldHint ?? true, }; } const title = formatToolTitle(toolName); // Read-only tools if (toolName.includes("list") || toolName.includes("get") || toolName === "system_info") { return { title, readOnlyHint: true, destructiveHint: false, openWorldHint: true }; } // Potentially destructive tools if (toolName.includes("delete") || toolName.includes("uninstall") || toolName.includes("remove")) { return { title, readOnlyHint: false, destructiveHint: true, openWorldHint: true }; } // Default: control tools that modify state return { title, readOnlyHint: false, destructiveHint: false, openWorldHint: true }; } type ServerConfig = z.infer<typeof configSchema>; /** * Creates the MCP server instance with all tools, resources, and prompts * Required by Smithery's TypeScript runtime */ export default function createServer({ config }: { config?: ServerConfig } = {}) { // Apply configuration to environment variables for downstream modules if (config?.hassToken) { process.env.HASS_TOKEN = config.hassToken; } if (config?.hassHost) { process.env.HASS_HOST = config.hassHost; } if (config?.hassSocketUrl) { process.env.HASS_SOCKET_URL = config.hassSocketUrl; } if (config?.debug) { process.env.DEBUG = "true"; } // Create the MCP server using the official SDK const server = new McpServer({ name: "Home Assistant MCP Server", version: "1.2.1", }); // Register all tools with proper MCP annotations and descriptions for (const tool of tools) { // Use tool's annotations if provided, otherwise generate from name const annotations = getToolAnnotations(tool.name, tool.annotations as ToolAnnotations | undefined); // Convert Zod schema to JSON Schema for MCP let inputSchema: Record<string, unknown>; try { const schema = zodToJsonSchema(tool.parameters, { name: tool.name, $refStrategy: "none", }); // Extract the definition if wrapped if (schema && typeof schema === "object" && "definitions" in schema) { const defs = schema.definitions as Record<string, unknown>; inputSchema = (defs[tool.name] as Record<string, unknown>) || schema; } else { inputSchema = schema as Record<string, unknown>; } } catch { // Fallback to a basic schema inputSchema = { type: "object", properties: {}, additionalProperties: true }; } server.tool( tool.name, tool.description, inputSchema, async (args: Record<string, unknown>) => { try { // Check for token during execution if needed const hasToken = Boolean(process.env.HASS_TOKEN); if (!hasToken) { return { content: [ { type: "text" as const, text: JSON.stringify({ error: "Home Assistant token not configured", message: "Please configure hassToken in the server settings to use this tool.", }), }, ], isError: true, }; } const result = await tool.execute(args as never); return { content: [ { type: "text" as const, text: typeof result === "string" ? result : JSON.stringify(result, null, 2), }, ], }; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); return { content: [ { type: "text" as const, text: JSON.stringify({ error: errorMsg }), }, ], isError: true, }; } } ); } // Add system_info tool server.tool( "system_info", "Get basic information about this MCP server including version, capabilities, and Home Assistant connection status", { type: "object", properties: {}, additionalProperties: false, }, async () => { const hasToken = Boolean(process.env.HASS_TOKEN); const hassHost = process.env.HASS_HOST || "not configured"; return { content: [ { type: "text" as const, text: JSON.stringify( { name: "Home Assistant MCP Server", version: "1.2.1", description: "Control your smart home through AI assistants", hassHost, connected: hasToken, toolCount: tools.length + 1, capabilities: ["tools", "resources", "prompts"], }, null, 2 ), }, ], }; } ); // Register all resources const resourceList = [ { uri: "ha://devices/all", name: "All Devices", description: "Complete list of all Home Assistant devices and their current states", mimeType: "application/json" }, { uri: "ha://devices/lights", name: "All Lights", description: "All light entities and their current states", mimeType: "application/json" }, { uri: "ha://devices/climate", name: "Climate Devices", description: "All climate control devices (thermostats, HVAC)", mimeType: "application/json" }, { uri: "ha://devices/media_players", name: "Media Players", description: "All media player entities and their states", mimeType: "application/json" }, { uri: "ha://devices/covers", name: "Covers", description: "All cover entities (blinds, curtains, garage doors)", mimeType: "application/json" }, { uri: "ha://devices/locks", name: "Locks", description: "All lock entities and their states", mimeType: "application/json" }, { uri: "ha://devices/fans", name: "Fans", description: "All fan entities and their states", mimeType: "application/json" }, { uri: "ha://devices/vacuums", name: "Vacuum Cleaners", description: "All vacuum entities and their states", mimeType: "application/json" }, { uri: "ha://devices/alarms", name: "Alarm Panels", description: "All alarm control panel entities", mimeType: "application/json" }, { uri: "ha://devices/sensors", name: "Sensors", description: "All sensor entities (temperature, humidity, etc.)", mimeType: "application/json" }, { uri: "ha://devices/switches", name: "Switches", description: "All switch entities", mimeType: "application/json" }, { uri: "ha://config/areas", name: "Areas/Rooms", description: "Configured areas and rooms in Home Assistant", mimeType: "application/json" }, { uri: "ha://config/automations", name: "Automations", description: "List of all configured automations", mimeType: "application/json" }, { uri: "ha://config/scenes", name: "Scenes", description: "List of all configured scenes", mimeType: "application/json" }, { uri: "ha://summary/dashboard", name: "Dashboard Summary", description: "Quick overview of home status including active devices, temperatures, and security status", mimeType: "application/json" }, ]; for (const resource of resourceList) { server.resource( resource.name, resource.uri, async (uri: URL) => { try { const content = await getResource(uri.href); if (!content) { return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "Resource not found" }) }] }; } return { contents: [content] }; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: errorMsg }) }] }; } } ); } // Register all prompts const allPrompts = getAllPrompts(); for (const prompt of allPrompts) { const promptArgs = prompt.arguments?.map(arg => ({ name: arg.name, description: arg.description, required: arg.required, })) || []; server.prompt( prompt.name, prompt.description, promptArgs, async (args: Record<string, string>) => { const rendered = renderPrompt(prompt.name, args); return { messages: [ { role: "user" as const, content: { type: "text" as const, text: rendered, }, }, ], }; } ); } // Return the underlying server object as required by Smithery return server.server; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jango-blockchained/advanced-homeassistant-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server