Initial commit
This commit is contained in:
561
IMPLEMENTATION_NOTES.md
Normal file
561
IMPLEMENTATION_NOTES.md
Normal file
@@ -0,0 +1,561 @@
|
||||
# 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! 🚀
|
||||
Reference in New Issue
Block a user