Add Docker support and enhance configuration management

- Introduced Dockerfile for building and running the application in a containerized environment.
- Added .dockerignore to exclude unnecessary files from the Docker context.
- Updated README.md with detailed Docker installation instructions and Node.js version management using nvm.
- Refactored environment variable handling in src/index.ts and src/config/hass.config.ts for improved configuration management.
- Enhanced TypeScript configuration to include JSON module resolution and updated exclusion patterns.
- Updated .gitignore to include additional files for better environment management.
This commit is contained in:
jango-blockchained
2024-12-16 14:37:25 +01:00
parent c7dd825104
commit 344c43a22f
8 changed files with 219 additions and 35 deletions

30
.dockerignore Normal file
View File

@@ -0,0 +1,30 @@
# Dependencies
node_modules
npm-debug.log
# Build outputs
dist
# Environment and config
.env
.env.*
# Version control
.git
.gitignore
# IDE
.vscode
.idea
# Testing
coverage
__tests__
# Logs
*.log
# Documentation
README.md
LICENSE
CHANGELOG.md

2
.gitignore vendored
View File

@@ -20,7 +20,7 @@ wheels/
*.egg-info/
.installed.cfg
*.egg
.cursorrules
# Environment variables
.env
.env.local

38
Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
# Build stage
FROM node:20.10.0-alpine AS builder
WORKDIR /app
# Install TypeScript globally
RUN npm install -g typescript
# Copy source files first
COPY . .
# Install all dependencies (including dev dependencies)
RUN npm install
# Build the project
RUN npm run build
# Production stage
FROM node:20.10.0-alpine
WORKDIR /app
# Set Node options for better compatibility
ENV NODE_OPTIONS="--experimental-modules"
ENV NODE_ENV="production"
# Copy package files and install production dependencies
COPY package*.json ./
RUN npm install --omit=dev --ignore-scripts
# Copy built files from builder stage
COPY --from=builder /app/dist ./dist
# Expose default port
EXPOSE 3000
# Start the server
CMD ["node", "dist/index.js"]

View File

@@ -16,23 +16,99 @@ The server uses the MCP protocol to share access to a local Home Assistant insta
## Prerequisites
- Node.js 20.10.0 or higher
- Node.js 20.10.0 or higher (Required for Array.prototype.toSorted())
- NPM package manager
- A running Home Assistant instance
- A long-lived access token from Home Assistant
### Node.js Version Management
If you're using an older version of Node.js, you can use `nvm` (Node Version Manager) to install and use the correct version:
```bash
# Install nvm (if not already installed)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# Reload shell configuration
source ~/.bashrc # or source ~/.zshrc for Zsh
# Install Node.js 20.10.0
nvm install 20.10.0
# Use Node.js 20.10.0
nvm use 20.10.0
```
## Installation
### Classic Installation
```bash
# Clone the repository
git clone https://github.com/jango-blockchained/homeassistant-mcp.git
cd homeassistant-mcp
# Install dependencies
yarn install
npm install
# Build the server
yarn build
npm run build
```
### Docker Installation
#### Using Docker Compose (Recommended)
1. Clone the repository:
```bash
git clone https://github.com/jango-blockchained/homeassistant-mcp.git
cd homeassistant-mcp
```
2. Create a `.env` file with your Home Assistant configuration:
```env
NODE_ENV=production
HASS_HOST=your_home_assistant_url
HASS_TOKEN=your_home_assistant_token
```
3. Start the container:
```bash
docker-compose up -d
```
#### Using Docker Directly
1. Build the image:
```bash
docker build -t homeassistant-mcp .
```
2. Run the container:
```bash
docker run -d \
--name homeassistant-mcp \
-e HASS_HOST=your_home_assistant_url \
-e HASS_TOKEN=your_home_assistant_token \
-p 3000:3000 \
homeassistant-mcp
```
### Docker Management Commands
```bash
# Stop the container
docker-compose down
# View logs
docker-compose logs -f
# Restart the container
docker-compose restart
# Update to latest version
git pull
docker-compose up -d --build
```
## Configuration
@@ -284,20 +360,16 @@ yarn test
### Common Issues
1. **Connection Errors**
1. **Node.js Version Error (`positive.toSorted is not a function`)**
- This error occurs when using Node.js version lower than 20
- Solution: Update to Node.js 20.10.0 or higher using nvm (see Prerequisites section)
- Docker users: The container automatically uses the correct Node.js version
2. **Connection Errors**
- Verify your Home Assistant instance is running
- Check the HASS_HOST is correct and accessible
- Ensure your token has the required permissions
2. **Entity Control Issues**
- Verify the entity_id exists in Home Assistant
- Check the entity domain matches the command
- Ensure parameter values are within valid ranges
3. **Permission Issues**
- Verify your token has write permissions for the entity
- Check Home Assistant logs for authorization errors
## Project Status
### Completed

View File

@@ -0,0 +1,6 @@
export const HASS_CONFIG = {
BASE_URL: process.env.HASS_HOST || 'http://192.168.178.63:8123',
TOKEN: process.env.HASS_TOKEN,
SOCKET_URL: process.env.HASS_HOST || 'http://192.168.178.63:8123',
SOCKET_TOKEN: process.env.HASS_TOKEN,
};

View File

@@ -1,6 +1,7 @@
import { CreateApplication, TServiceParams, StringConfig } from "@digital-alchemy/core";
import { LIB_HASS, PICK_ENTITY } from "@digital-alchemy/hass";
import { DomainSchema } from "../schemas.js";
import { HASS_CONFIG } from "../config/hass.config.js";
type Environments = "development" | "production" | "test";
@@ -25,19 +26,39 @@ const MY_APP = CreateApplication({
enum: ["development", "production", "test"],
description: "Code runner addon can set with it's own NODE_ENV",
} satisfies StringConfig<Environments>,
HASS_HOST: {
type: "string",
description: "Home Assistant host URL",
required: true
},
HASS_TOKEN: {
type: "string",
description: "Home Assistant long-lived access token",
required: true
}
},
services: {},
libraries: [LIB_HASS],
libraries: [
{
...LIB_HASS,
configuration: {
BASE_URL: {
type: "string",
description: "Home Assistant base URL",
required: true,
default: HASS_CONFIG.BASE_URL
},
TOKEN: {
type: "string",
description: "Home Assistant long-lived access token",
required: true,
default: HASS_CONFIG.TOKEN
},
SOCKET_URL: {
type: "string",
description: "Home Assistant WebSocket URL",
required: true,
default: HASS_CONFIG.SOCKET_URL
},
SOCKET_TOKEN: {
type: "string",
description: "Home Assistant WebSocket token",
required: true,
default: HASS_CONFIG.SOCKET_TOKEN
}
}
}
],
name: 'hass' as const
});

View File

@@ -3,6 +3,10 @@ import { LiteMCP } from 'litemcp';
import { z } from 'zod';
import { DomainSchema } from './schemas.js';
// Configuration
const HASS_HOST = process.env.HASS_HOST || 'http://192.168.178.63:8123';
const HASS_TOKEN = process.env.HASS_TOKEN;
interface CommandParams {
command: string;
entity_id: string;
@@ -41,9 +45,9 @@ async function main() {
parameters: z.object({}),
execute: async () => {
try {
const response = await fetch(`${process.env.HASS_HOST}/api/states`, {
const response = await fetch(`${HASS_HOST}/api/states`, {
headers: {
Authorization: `Bearer ${process.env.HASS_TOKEN}`,
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
@@ -185,15 +189,26 @@ async function main() {
// Call Home Assistant service
try {
await hass.services[domain][service](serviceData);
} catch (error) {
throw new Error(`Failed to execute ${service} for ${params.entity_id}: ${error instanceof Error ? error.message : 'Unknown error occurred'}`);
const response = await fetch(`${HASS_HOST}/api/services/${domain}/${service}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(serviceData),
});
if (!response.ok) {
throw new Error(`Failed to execute ${service} for ${params.entity_id}: ${response.statusText}`);
}
return {
success: true,
message: `Successfully executed ${service} for ${params.entity_id}`
};
} catch (error) {
throw new Error(`Failed to execute ${service} for ${params.entity_id}: ${error instanceof Error ? error.message : 'Unknown error occurred'}`);
}
} catch (error) {
return {
success: false,

View File

@@ -9,14 +9,16 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"allowJs": true
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
"dist",
"__tests__"
]
}