Skip to main content
Glama
resources.ts15.8 kB
import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { getAllStates, getHomeAssistantState, formatErrorResponse, formatSuccessResponse } from "../../utils/api.js"; // Domain-specific important attributes for lean responses const DOMAIN_IMPORTANT_ATTRIBUTES: { [key: string]: string[] } = { "light": ["brightness", "color_temp", "rgb_color", "supported_color_modes"], "switch": ["device_class"], "binary_sensor": ["device_class"], "sensor": ["device_class", "unit_of_measurement", "state_class"], "climate": ["hvac_mode", "current_temperature", "temperature", "hvac_action"], "media_player": ["media_title", "media_artist", "source", "volume_level"], "cover": ["current_position", "current_tilt_position"], "fan": ["percentage", "preset_mode"], "camera": ["entity_picture"], "automation": ["last_triggered"], "scene": [], "script": ["last_triggered"] }; /** * Register resource-related tools for Home Assistant */ export function registerResourceTools(server: McpServer) { // Tool to get specific resource by URI server.tool( "homeassistant_get_resource", "Get a specific Home Assistant resource by URI", { uri: z.string().describe("The resource URI (e.g., 'hass://entities/light.living_room', 'hass://entities/domain/light')") }, async ({ uri }: { uri: string }) => { console.error(`Getting resource: ${uri}`); try { // Parse the URI to determine resource type const url = new URL(uri); const pathParts = url.pathname.split('/').filter(p => p); if (url.protocol !== 'hass:') { return formatErrorResponse(`Invalid protocol. Must use 'hass:' protocol`); } // Handle different resource types if (pathParts[0] === 'entities') { if (pathParts.length === 1) { // hass://entities - all entities overview return await handleAllEntitiesResource(); } else if (pathParts.length === 2) { // hass://entities/{entity_id} - specific entity const entityId = pathParts[1]; return await handleEntityResource(entityId, false); } else if (pathParts.length === 3 && pathParts[2] === 'detailed') { // hass://entities/{entity_id}/detailed - detailed entity view const entityId = pathParts[1]; return await handleEntityResource(entityId, true); } else if (pathParts.length === 3 && pathParts[1] === 'domain') { // hass://entities/domain/{domain} - domain entities const domain = pathParts[2]; return await handleDomainResource(domain); } else if (pathParts.length === 4 && pathParts[1] === 'domain' && pathParts[3] === 'summary') { // hass://entities/domain/{domain}/summary - domain summary const domain = pathParts[2]; return await handleDomainSummaryResource(domain); } } else if (pathParts[0] === 'search') { if (pathParts.length >= 2) { // hass://search/{query}/{limit?} - entity search const query = decodeURIComponent(pathParts[1]); const limit = pathParts.length > 2 ? parseInt(pathParts[2]) || 20 : 20; return await handleSearchResource(query, limit); } } return formatErrorResponse(`Unknown resource URI pattern: ${uri}`); } catch (error: any) { return formatErrorResponse(`Error processing resource URI: ${error.message}`); } } ); // Tool to list available resources server.tool( "homeassistant_list_resources", "List all available Home Assistant resources with their URIs", {}, async () => { const resources = [ { uri: "hass://entities", description: "All entities overview with domain grouping", note: "Large response - consider using domain-specific or search resources" }, { uri: "hass://entities/{entity_id}", description: "Individual entity state and key attributes", example: "hass://entities/light.living_room" }, { uri: "hass://entities/{entity_id}/detailed", description: "Complete entity information including all attributes", example: "hass://entities/light.living_room/detailed" }, { uri: "hass://entities/domain/{domain}", description: "All entities in a specific domain", example: "hass://entities/domain/light" }, { uri: "hass://entities/domain/{domain}/summary", description: "Statistical summary of entities in a domain", example: "hass://entities/domain/sensor/summary" }, { uri: "hass://search/{query}/{limit}", description: "Search entities by ID, name, or state (limit optional, default 20)", example: "hass://search/living%20room/10" } ]; let content = "# Available Home Assistant Resources\n\n"; resources.forEach((resource, index) => { content += `## ${index + 1}. ${resource.uri}\n\n`; content += `**Description**: ${resource.description}\n\n`; if ((resource as any).example) { content += `**Example**: \`${(resource as any).example}\`\n\n`; } if ((resource as any).note) { content += `**Note**: ${(resource as any).note}\n\n`; } }); content += "## Usage Tips\n\n"; content += "- Use specific entity URIs for best performance\n"; content += "- Domain resources are efficient for exploring entity types\n"; content += "- Search resources support URL encoding for special characters\n"; content += "- Detailed views provide complete information but use more tokens\n"; return formatSuccessResponse(content); } ); // Helper functions for resource handling async function handleAllEntitiesResource() { const result = await getAllStates(); if (!result.success) { return formatErrorResponse(`Error retrieving entities: ${result.message}`); } const entities = result.data; let content = "# Home Assistant Entities Overview\n\n"; content += `Total entities: ${entities.length}\n\n`; content += "⚠️ **Performance Note**: This is a complete entity list. For better performance, consider:\n"; content += "- Domain filtering: `hass://entities/domain/{domain}`\n"; content += "- Entity search: `hass://search/{query}`\n"; content += "- Specific entities: `hass://entities/{entity_id}`\n\n"; // Group entities by domain const domains: { [key: string]: any[] } = {}; entities.forEach((entity: any) => { const domain = entity.entity_id.split('.')[0]; if (!domains[domain]) { domains[domain] = []; } domains[domain].push(entity); }); Object.entries(domains).sort(([a], [b]) => a.localeCompare(b)).forEach(([domain, domainEntities]) => { content += `## ${domain.toUpperCase()} (${domainEntities.length} entities)\n\n`; // Show first 5 entities of each domain domainEntities.slice(0, 5).forEach(entity => { const name = entity.attributes?.friendly_name || entity.entity_id; content += `- **${entity.entity_id}**: ${entity.state} (${name})\n`; }); if (domainEntities.length > 5) { content += `- ... and ${domainEntities.length - 5} more ${domain} entities\n`; content += `- [View all ${domain} entities](hass://entities/domain/${domain})\n`; } content += "\n"; }); return formatSuccessResponse(content); } async function handleEntityResource(entityId: string, detailed: boolean) { const result = await getHomeAssistantState(entityId); if (!result.success) { return formatErrorResponse(`Error retrieving entity ${entityId}: ${result.message}`); } const entity = result.data; const domain = entityId.split(".")[0]; let content = `# Entity: ${entityId}${detailed ? ' (Detailed)' : ''}\n\n`; const friendlyName = entity.attributes?.friendly_name; if (friendlyName && friendlyName !== entityId) { content += `**Name**: ${friendlyName}\n\n`; } content += `**State**: ${entity.state}\n`; content += `**Domain**: ${domain}\n\n`; if (detailed) { content += "## All Attributes\n\n"; const attributes = entity.attributes || {}; if (Object.keys(attributes).length > 0) { Object.entries(attributes).forEach(([key, value]) => { content += `- **${key}**: ${JSON.stringify(value)}\n`; }); } else { content += "*No attributes available*\n"; } content += "\n## Context Information\n\n"; if (entity.last_updated) { content += `**Last Updated**: ${entity.last_updated}\n`; } if (entity.last_changed) { content += `**Last Changed**: ${entity.last_changed}\n`; } } else { content += "## Key Attributes\n\n"; const attributes = entity.attributes || {}; const importantAttrs = DOMAIN_IMPORTANT_ATTRIBUTES[domain] || []; let displayedAttrs = 0; for (const attrName of importantAttrs) { if (attributes[attrName] !== undefined) { content += `- **${attrName}**: ${JSON.stringify(attributes[attrName])}\n`; displayedAttrs++; } } if (displayedAttrs === 0) { content += "*No key attributes for this entity type*\n"; } content += `\n[View detailed information](hass://entities/${entityId}/detailed)\n`; } return formatSuccessResponse(content); } async function handleDomainResource(domain: string) { const result = await getAllStates(); if (!result.success) { return formatErrorResponse(`Error retrieving entities: ${result.message}`); } const allEntities = result.data; const entities = allEntities.filter((entity: any) => entity.entity_id.startsWith(`${domain}.`) ); if (entities.length === 0) { return formatErrorResponse(`No entities found for domain: ${domain}`); } let content = `# ${domain.charAt(0).toUpperCase() + domain.slice(1)} Domain Entities\n\n`; content += `Total entities: ${entities.length}\n\n`; entities.forEach((entity: any) => { const name = entity.attributes?.friendly_name || entity.entity_id; content += `## ${entity.entity_id}\n`; content += `- **Name**: ${name}\n`; content += `- **State**: ${entity.state}\n`; // Add important attributes for this domain const importantAttrs = DOMAIN_IMPORTANT_ATTRIBUTES[domain] || []; importantAttrs.forEach(attr => { if (entity.attributes?.[attr] !== undefined) { content += `- **${attr}**: ${entity.attributes[attr]}\n`; } }); content += "\n"; }); content += `## Related Resources\n\n`; content += `- [Domain summary](hass://entities/domain/${domain}/summary)\n`; content += `- [Search in this domain](hass://search/${domain}/20)\n`; return formatSuccessResponse(content); } async function handleDomainSummaryResource(domain: string) { const result = await getAllStates(); if (!result.success) { return formatErrorResponse(`Error retrieving entities: ${result.message}`); } const allEntities = result.data; const entities = allEntities.filter((entity: any) => entity.entity_id.startsWith(`${domain}.`) ); if (entities.length === 0) { return formatErrorResponse(`No entities found for domain: ${domain}`); } // Analyze the domain const stateCounts: { [key: string]: number } = {}; const attributesSummary: { [key: string]: Set<any> } = {}; entities.forEach((entity: any) => { const state = entity.state || "unknown"; stateCounts[state] = (stateCounts[state] || 0) + 1; if (entity.attributes) { Object.entries(entity.attributes).forEach(([key, value]) => { if (!attributesSummary[key]) { attributesSummary[key] = new Set(); } if (value !== null && value !== undefined) { attributesSummary[key].add(typeof value === 'object' ? JSON.stringify(value) : value); } }); } }); let content = `# ${domain.charAt(0).toUpperCase() + domain.slice(1)} Domain Summary\n\n`; content += `**Total entities**: ${entities.length}\n\n`; // State distribution content += "## State Distribution\n\n"; Object.entries(stateCounts).forEach(([state, count]) => { const percentage = ((count / entities.length) * 100).toFixed(1); content += `- **${state}**: ${count} entities (${percentage}%)\n`; }); // Common attributes content += "\n## Common Attributes\n\n"; const sortedAttributes = Object.entries(attributesSummary) .sort(([, a], [, b]) => b.size - a.size) .slice(0, 10); sortedAttributes.forEach(([attr, values]) => { const valueCount = values.size; content += `- **${attr}**: ${valueCount} unique values\n`; if (valueCount <= 5) { const valuesList = Array.from(values).slice(0, 5); content += ` Values: ${valuesList.join(', ')}\n`; } }); content += `\n## Related Resources\n\n`; content += `- [View all ${domain} entities](hass://entities/domain/${domain})\n`; content += `- [Search ${domain} entities](hass://search/${domain}/20)\n`; return formatSuccessResponse(content); } async function handleSearchResource(query: string, limit: number) { if (!query || query.trim() === "") { return formatErrorResponse("Please provide a search query"); } const result = await getAllStates(); if (!result.success) { return formatErrorResponse(`Error searching entities: ${result.message}`); } const searchTerm = query.toLowerCase().trim(); const matchingEntities = result.data.filter((entity: any) => { const entityId = entity.entity_id.toLowerCase(); const friendlyName = (entity.attributes?.friendly_name || "").toLowerCase(); const state = (entity.state || "").toLowerCase(); return entityId.includes(searchTerm) || friendlyName.includes(searchTerm) || state.includes(searchTerm); }).slice(0, limit); if (matchingEntities.length === 0) { return formatSuccessResponse(`No entities found matching: '${query}'`); } let content = `# Search Results for '${query}'\n\n`; content += `Found ${matchingEntities.length} matching entities (limit: ${limit}):\n\n`; // Group by domain const domains: { [key: string]: any[] } = {}; matchingEntities.forEach((entity: any) => { const domain = entity.entity_id.split('.')[0]; if (!domains[domain]) { domains[domain] = []; } domains[domain].push(entity); }); Object.entries(domains).forEach(([domain, domainEntities]) => { content += `## ${domain.toUpperCase()} (${domainEntities.length})\n\n`; domainEntities.forEach(entity => { const name = entity.attributes?.friendly_name || entity.entity_id; content += `- [${entity.entity_id}](hass://entities/${entity.entity_id}): ${entity.state}\n`; if (name !== entity.entity_id) { content += ` Name: ${name}\n`; } }); content += "\n"; }); return formatSuccessResponse(content); } }

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/gilberth/enhanced-homeassistant-mcp'

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