#!/usr/bin/env node
/**
* MCP Server for Homey API
*
* This MCP server enables AI agents to interact with Homey home automation:
* - List and manage flows
* - Control devices
* - View insights
* - Manage zones
*
* Authentication: OAuth2 via Homey Cloud API
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { AthomCloudAPI } from 'athom-api';
import { HomeyAPIV3 } from 'homey-api';
import * as dotenv from 'dotenv';
// Load environment variables
dotenv.config();
// Type definitions
interface FlowUpdateParams {
id: string;
name?: string;
enabled?: boolean;
folder?: string;
}
interface FlowListParams {
folder?: string;
enabled?: boolean;
}
// Global Homey API instance
let homeyAPI: HomeyAPIV3.HomeyAPIV3 | null = null;
let cloudAPI: AthomCloudAPI | null = null;
/**
* Initialize Homey API connection
*/
async function initializeHomeyAPI(): Promise<HomeyAPIV3.HomeyAPIV3> {
if (homeyAPI) {
return homeyAPI;
}
const clientId = process.env.HOMEY_CLIENT_ID;
const clientSecret = process.env.HOMEY_CLIENT_SECRET;
const homeyId = process.env.HOMEY_ID;
if (!clientId || !clientSecret) {
throw new Error(
'HOMEY_CLIENT_ID and HOMEY_CLIENT_SECRET must be set in .env file. ' +
'Get credentials from https://tools.developer.homey.app/'
);
}
if (!homeyId) {
throw new Error(
'HOMEY_ID must be set in .env file. ' +
'Find it in your Homey URL: https://my.homey.app/homeys/YOUR_HOMEY_ID/flows'
);
}
// Initialize Cloud API
cloudAPI = new AthomCloudAPI({
clientId,
clientSecret,
});
// Authenticate (will prompt for login if needed)
const user = await cloudAPI.authenticateWithAuthorizationCode({
email: process.env.HOMEY_EMAIL || '',
password: process.env.HOMEY_PASSWORD || '',
});
console.error('✅ Authenticated as:', user.firstname, user.lastname);
// Get Homey instance
const homey = await cloudAPI.getHomey({ id: homeyId });
// Create Homey API
homeyAPI = await HomeyAPIV3.createAPI({ homey });
console.error('✅ Connected to Homey:', await homeyAPI.system.getSystemName());
return homeyAPI;
}
/**
* Define available MCP tools
*/
const TOOLS: Tool[] = [
{
name: 'list_flows',
description: 'List all Homey flows with optional filtering',
inputSchema: {
type: 'object',
properties: {
folder: {
type: 'string',
description: 'Filter by folder path (e.g., "binnen/begane grond")',
},
enabled: {
type: 'boolean',
description: 'Filter by enabled status',
},
},
},
},
{
name: 'get_flow',
description: 'Get detailed information about a specific flow',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Flow ID',
},
},
required: ['id'],
},
},
{
name: 'update_flow',
description: 'Update a flow (rename, enable/disable, move to folder)',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Flow ID',
},
name: {
type: 'string',
description: 'New flow name',
},
enabled: {
type: 'boolean',
description: 'Enable or disable the flow',
},
folder: {
type: 'string',
description: 'Move to folder ID',
},
},
required: ['id'],
},
},
{
name: 'list_flow_folders',
description: 'List all flow folders in hierarchical structure',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'trigger_flow',
description: 'Manually trigger a flow',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Flow ID',
},
},
required: ['id'],
},
},
{
name: 'list_devices',
description: 'List all Homey devices',
inputSchema: {
type: 'object',
properties: {
zone: {
type: 'string',
description: 'Filter by zone name',
},
},
},
},
{
name: 'list_zones',
description: 'List all Homey zones in hierarchical structure',
inputSchema: {
type: 'object',
properties: {},
},
},
];
/**
* Create and configure MCP server
*/
const server = new Server(
{
name: 'mcp-server-homey',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
/**
* Handler: List available tools
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: TOOLS,
};
});
/**
* Handler: Execute tool
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const api = await initializeHomeyAPI();
switch (name) {
case 'list_flows': {
const params = args as FlowListParams;
const flows = await api.flow.getFlows();
let result = Object.values(flows);
// Apply filters
if (params.enabled !== undefined) {
result = result.filter(f => f.enabled === params.enabled);
}
// Format output
const formatted = result.map(flow => ({
id: flow.id,
name: flow.name,
enabled: flow.enabled,
folder: flow.folder,
type: flow.type,
triggerable: flow.triggerable,
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(formatted, null, 2),
},
],
};
}
case 'get_flow': {
const { id } = args as { id: string };
const flow = await api.flow.getFlow({ id });
return {
content: [
{
type: 'text',
text: JSON.stringify(flow, null, 2),
},
],
};
}
case 'update_flow': {
const params = args as FlowUpdateParams;
const { id, ...updates } = params;
// Get current flow
const currentFlow = await api.flow.getFlow({ id });
// Update flow
const updatedFlow = await api.flow.updateFlow({
id,
flow: {
...currentFlow,
...updates,
},
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
flow: {
id: updatedFlow.id,
name: updatedFlow.name,
enabled: updatedFlow.enabled,
folder: updatedFlow.folder,
},
}, null, 2),
},
],
};
}
case 'list_flow_folders': {
const folders = await api.flow.getFlowFolders();
// Build hierarchical structure
const formatted = Object.values(folders).map(folder => ({
id: folder.id,
name: folder.name,
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(formatted, null, 2),
},
],
};
}
case 'trigger_flow': {
const { id } = args as { id: string };
const flow = await api.flow.getFlow({ id });
if (!flow.triggerable) {
throw new Error(`Flow "${flow.name}" is not triggerable`);
}
await flow.trigger();
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: `Flow "${flow.name}" triggered successfully`,
}, null, 2),
},
],
};
}
case 'list_devices': {
const params = args as { zone?: string };
const devices = await api.devices.getDevices();
let result = Object.values(devices);
// Filter by zone if specified
if (params.zone) {
result = result.filter(d => d.zone === params.zone);
}
const formatted = result.map(device => ({
id: device.id,
name: device.name,
zone: device.zone,
class: device.class,
available: device.available,
capabilities: device.capabilities,
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(formatted, null, 2),
},
],
};
}
case 'list_zones': {
const zones = await api.zones.getZones();
const formatted = Object.values(zones).map(zone => ({
id: zone.id,
name: zone.name,
parent: zone.parent,
active: zone.active,
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(formatted, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: errorMessage,
}, null, 2),
},
],
isError: true,
};
}
});
/**
* Start server
*/
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('🚀 Homey MCP Server running on stdio');
console.error('📚 Available tools:', TOOLS.map(t => t.name).join(', '));
}
main().catch((error) => {
console.error('❌ Fatal error:', error);
process.exit(1);
});