# Implementation Notes for Home Assistant MCP Server ## Developer Guide for Understanding and Extending the Codebase ### Architecture Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ MCP Client │ │ (Claude Desktop / Other MCP Client) │ └───────────────────────────┬─────────────────────────────────┘ │ │ stdio (JSON-RPC over stdin/stdout) │ ┌───────────────────────────▼─────────────────────────────────┐ │ MCP Server Layer │ │ (src/index.ts) │ ├──────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────┐ ┌────────────────┐ ┌──────────────┐ │ │ │ Resources │ │ Tools │ │ Error │ │ │ │ Handler │ │ Handler │ │ Handler │ │ │ └────────┬───────┘ └────────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └───────────────────┼───────────────────┘ │ │ │ │ └───────────────────────────────┼──────────────────────────────┘ │ ┌───────────────────────────────▼──────────────────────────────┐ │ Home Assistant Client │ │ (src/ha-client.ts) │ ├──────────────────────────────────────────────────────────────┤ │ │ │ • HTTP Request Building • Response Parsing │ │ • Authentication Headers • Error Formatting │ │ • Type-safe Methods • Timeout Management │ │ │ └───────────────────────────────┬──────────────────────────────┘ │ │ HTTP/HTTPS + Bearer Token │ ┌───────────────────────────────▼──────────────────────────────┐ │ Home Assistant REST API │ │ (port 8123) │ └──────────────────────────────────────────────────────────────┘ ``` ### Key Design Patterns #### 1. Layered Architecture **Layer 1: MCP Protocol Handler** (`src/index.ts`) - Handles MCP protocol requirements - Validates JSON schemas - Manages request routing - Formats responses according to MCP spec **Layer 2: Home Assistant Client** (`src/ha-client.ts`) - Abstracts HTTP communication - Provides type-safe methods - Handles authentication - Formats errors consistently **Layer 3: Type System** (`src/types.ts`) - Defines data structures - Documents API contracts - Enables IDE autocomplete - Catches errors at compile time #### 2. Request Flow Pattern Every MCP request follows this flow: ```typescript // 1. Request arrives via stdio MCP Client sends JSON-RPC request ↓ // 2. Server routes to handler server.setRequestHandler(Schema, async (request) => { ↓ // 3. Extract and validate parameters const { param1, param2 } = request.params; ↓ // 4. Call HA client method const result = await haClient.someMethod(param1, param2); ↓ // 5. Format as MCP response return { content: [{ type: 'text', text: JSON.stringify(result) }] }; ↓ // 6. Error handling wraps everything } catch (error) { throw new McpError(ErrorCode.InternalError, formatError(error)); } ``` #### 3. Error Handling Pattern Three-tier error handling: ```typescript // Tier 1: Try-catch in handler try { const result = await haClient.method(); } catch (error) { // Tier 2: Check if already MCP error if (error instanceof McpError) throw error; // Tier 3: Format and wrap as MCP error throw new McpError( ErrorCode.InternalError, HomeAssistantClient.formatError(error) ); } ``` ### Code Organization #### index.ts Structure ```typescript // 1. Imports and initialization import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { HomeAssistantClient } from './ha-client.js'; // 2. Configuration loading const config = loadConfig(); const haClient = new HomeAssistantClient(config); // 3. Server initialization const server = new Server({ name, version }, { capabilities }); // 4. Resource handlers server.setRequestHandler(ListResourcesRequestSchema, ...); server.setRequestHandler(ReadResourceRequestSchema, ...); // 5. Tool handlers server.setRequestHandler(ListToolsRequestSchema, ...); server.setRequestHandler(CallToolRequestSchema, ...); // 6. Startup async function main() { await haClient.checkAPI(); // Verify connection const transport = new StdioServerTransport(); await server.connect(transport); } ``` #### ha-client.ts Structure ```typescript export class HomeAssistantClient { private client: AxiosInstance; // Constructor sets up axios with auth constructor(config: HAConfig) { ... } // System information methods async getConfig(): Promise { ... } async getComponents(): Promise { ... } // State management methods async getStates(): Promise { ... } async getState(entityId: string): Promise { ... } async setState(...): Promise { ... } // Service execution methods async callService(...): Promise<...> { ... } // Utility methods static formatError(error: any): string { ... } } ``` ### Implementation Details #### 1. Authentication Implementation ```typescript // Configuration const client = axios.create({ baseURL: `${baseUrl}/api`, headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, timeout: 30000, }); // Automatic inclusion in all requests // No need to manually add header to each request ``` #### 2. Resource URI Handling ```typescript // Static resources (known at compile time) if (uri === 'ha://states') { const states = await haClient.getStates(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(states) }] }; } // Dynamic resources (entity-specific) if (uri.startsWith('ha://states/')) { const entityId = uri.replace('ha://states/', ''); const state = await haClient.getState(entityId); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(state) }] }; } ``` #### 3. Tool Parameter Extraction ```typescript // TypeScript type assertion with validation const { domain, service, service_data, return_response } = args as { domain: string; service: string; service_data?: Record; return_response?: boolean; }; // Use with confidence (types are validated by MCP SDK against schema) const result = await haClient.callService(domain, service, service_data, return_response); ``` #### 4. Response Formatting ```typescript // All tool responses use this format return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), // Pretty-printed JSON }, ], }; // Resources use a slightly different format return { contents: [ { uri: request.params.uri, mimeType: 'application/json', text: JSON.stringify(data, null, 2), }, ], }; ``` ### Extension Cookbook #### Adding a New Tool **Step 1: Define the tool schema** ```typescript // In ListToolsRequestSchema handler { name: 'new_tool', description: 'Clear description of what this tool does', inputSchema: { type: 'object', properties: { param1: { type: 'string', description: 'First parameter' }, param2: { type: 'number', description: 'Second parameter' }, }, required: ['param1'], }, } ``` **Step 2: Implement the handler** ```typescript // In CallToolRequestSchema handler, add to switch statement case 'new_tool': { const { param1, param2 } = args as { param1: string; param2?: number; }; const result = await haClient.newMethod(param1, param2); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2) }, ], }; } ``` **Step 3: Add client method (if needed)** ```typescript // In ha-client.ts async newMethod(param1: string, param2?: number): Promise { const response = await this.client.get('/new_endpoint', { params: { param1, param2 }, }); return response.data; } ``` **Step 4: Add types (if needed)** ```typescript // In types.ts export interface ResultType { field1: string; field2: number; } ``` #### Adding a New Resource **Step 1: Declare in list** ```typescript // In ListResourcesRequestSchema handler { uri: 'ha://new_resource', name: 'New Resource', description: 'Description of what this resource provides', mimeType: 'application/json', } ``` **Step 2: Implement read handler** ```typescript // In ReadResourceRequestSchema handler if (uri === 'ha://new_resource') { const data = await haClient.getNewData(); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(data, null, 2), }, ], }; } ``` ### Testing Strategies #### Manual Testing with MCP Inspector ```bash # Install MCP Inspector npm install -g @modelcontextprotocol/inspector # Run server with inspector npx @modelcontextprotocol/inspector node build/index.js ``` #### Direct Testing with Node ```typescript // test.ts import { HomeAssistantClient } from './src/ha-client.js'; const client = new HomeAssistantClient({ baseUrl: 'http://homeassistant.local:8123', accessToken: 'your_token', }); // Test individual methods const states = await client.getStates(); console.log(`Found ${states.length} entities`); const light = await client.getState('light.living_room'); console.log(`Light state: ${light.state}`); await client.callService('light', 'turn_on', { entity_id: 'light.living_room', brightness_pct: 50, }); ``` #### Testing with Claude Desktop 1. Configure Claude Desktop 2. Start conversation 3. Ask: "What resources do you have access to?" 4. Test specific tools: "Turn on light.living_room" 5. Check Claude Desktop logs for errors ### Common Pitfalls and Solutions #### Pitfall 1: Forgetting async/await ```typescript // ❌ Wrong - returns Promise instead of value const states = haClient.getStates(); // ✅ Correct const states = await haClient.getStates(); ``` #### Pitfall 2: Not handling errors ```typescript // ❌ Wrong - unhandled promise rejection const state = await haClient.getState('invalid.entity'); // ✅ Correct try { const state = await haClient.getState('invalid.entity'); } catch (error) { throw new McpError(ErrorCode.InternalError, formatError(error)); } ``` #### Pitfall 3: Incorrect URI parsing ```typescript // ❌ Wrong - doesn't handle edge cases const entityId = uri.split('/')[2]; // ✅ Correct - robust parsing const entityId = uri.replace('ha://states/', ''); ``` #### Pitfall 4: Missing type assertions ```typescript // ❌ Wrong - args is type unknown const domain = args.domain; // TypeScript error // ✅ Correct - assert types const { domain } = args as { domain: string }; ``` ### Performance Optimization Tips #### 1. Minimize API Calls ```typescript // ❌ Inefficient - multiple calls for (const id of entityIds) { await haClient.getState(id); } // ✅ Better - single call const states = await haClient.getStates(); const filtered = states.filter(s => entityIds.includes(s.entity_id)); ``` #### 2. Use Templates for Complex Queries ```typescript // ❌ Slow - fetch all, filter in Node const states = await haClient.getStates(); const count = states.filter(s => s.state === 'on').length; // ✅ Fast - let HA do the work const template = "{{ states | selectattr('state', 'eq', 'on') | list | count }}"; const count = await haClient.renderTemplate(template); ``` #### 3. Request Only What You Need ```typescript // ❌ Wasteful - fetches all attributes const history = await haClient.getHistory(timestamp, entities); // ✅ Efficient - minimal response const history = await haClient.getHistory( timestamp, entities, endTime, true, // minimal_response true, // no_attributes true // significant_changes_only ); ``` ### Debugging Tips #### Enable Verbose Logging ```typescript // Add to ha-client.ts constructor this.client.interceptors.request.use(request => { console.error(`[HA] ${request.method?.toUpperCase()} ${request.url}`); return request; }); this.client.interceptors.response.use( response => { console.error(`[HA] ${response.status} ${response.config.url}`); return response; }, error => { console.error(`[HA] Error: ${error.message}`); return Promise.reject(error); } ); ``` #### Check MCP Message Flow ```typescript // Add to index.ts handlers console.error('[MCP] Received request:', request.method); console.error('[MCP] Parameters:', JSON.stringify(request.params, null, 2)); ``` #### Inspect Home Assistant Logs ```bash # SSH to Home Assistant ssh root@homeassistant.local # View logs docker logs homeassistant # Or in HA UI: Settings > System > Logs ``` ### Security Considerations #### Token Security - Never log tokens - Never commit .env files - Rotate tokens regularly - Use separate tokens per application #### Input Validation - All user input is validated by JSON schema - Entity IDs are not sanitized (HA handles this) - Template injection is possible (by design) - No SQL injection risk (REST API) #### Network Security - Use HTTPS in production - Consider VPN for remote access - Firewall HA from internet - Use strong tokens (default is secure) ### MCP Best Practices Followed 1. **Clear Capability Declaration**: Server announces resources and tools upfront 2. **Descriptive Schemas**: All parameters have clear descriptions 3. **Consistent Error Handling**: McpError with appropriate codes 4. **Type Safety**: Strong typing throughout 5. **Stdio Transport**: Universal compatibility 6. **Stateless Design**: No session management needed 7. **Idempotent Resources**: Same URI always returns same data 8. **Documented Tools**: Each tool has clear purpose and examples ### Next Steps for Developers 1. **Read the MCP Spec**: https://modelcontextprotocol.io 2. **Study Home Assistant API**: https://developers.home-assistant.io/docs/api/rest/ 3. **Explore the Code**: Start with index.ts, follow the flow 4. **Test Locally**: Use setup.sh to get running 5. **Extend**: Add the tools/resources you need 6. **Contribute**: Share improvements back ### Useful Resources - **MCP SDK Docs**: https://github.com/modelcontextprotocol/typescript-sdk - **Home Assistant API**: https://developers.home-assistant.io/docs/api/rest/ - **TypeScript Handbook**: https://www.typescriptlang.org/docs/ - **Axios Documentation**: https://axios-http.com/docs/intro - **Zod Documentation**: https://zod.dev/ ### Conclusion This implementation provides a solid foundation for Home Assistant + MCP integration. The code is well-structured, type-safe, and follows best practices. Extending it should be straightforward by following the patterns established in the existing code. The separation of concerns between MCP protocol handling, HTTP communication, and type definitions makes the codebase maintainable and testable. Each layer has a clear responsibility and can be modified independently. Happy coding! 🚀