562 lines
17 KiB
Markdown
562 lines
17 KiB
Markdown
# 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<SystemConfig> { ... }
|
|
async getComponents(): Promise<string[]> { ... }
|
|
|
|
// State management methods
|
|
async getStates(): Promise<EntityState[]> { ... }
|
|
async getState(entityId: string): Promise<EntityState> { ... }
|
|
async setState(...): Promise<EntityState> { ... }
|
|
|
|
// 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<string, any>;
|
|
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<ResultType> {
|
|
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! 🚀
|