Skip to main content
Glama
server.js36.6 kB
#!/usr/bin/env node // GARZA OS Home MCP Server v1.1.0 - SECURED // Personal/home automation: Beeper, Abode, UniFi, ProtonMail, Graphiti, Bible, Pushcut const http = require('http'); const https = require('https'); const crypto = require('crypto'); const PORT = process.env.PORT || 8080; const VERSION = "1.1.0"; // ============ SECURITY CONFIG ============ // API Keys - NO FALLBACKS (must be set in environment) const API_KEY = process.env.MCP_API_KEY; const ADMIN_KEY = process.env.MCP_ADMIN_KEY; // Optional: for sensitive operations if (!API_KEY) { console.error('FATAL: MCP_API_KEY environment variable required'); process.exit(1); } // Rate limiting config const RATE_LIMIT_WINDOW = 60000; // 1 minute const RATE_LIMIT_MAX = 100; // requests per window const rateLimitMap = new Map(); // Audit log (in-memory, rotates at 1000 entries) const auditLog = []; const MAX_AUDIT_LOG = 1000; // Session expiration (1 hour) const SESSION_EXPIRY = 3600000; // Service credentials const BEEPER_TOKEN = process.env.BEEPER_TOKEN; const ABODE_USER = process.env.ABODE_USER; const ABODE_PASS = process.env.ABODE_PASS; const UNIFI_HOST = process.env.UNIFI_HOST || '192.168.10.49'; const UNIFI_USER = process.env.UNIFI_USER; const UNIFI_PASS = process.env.UNIFI_PASS; const GRAPHITI_URL = process.env.GRAPHITI_URL; const BIBLE_API_KEY = process.env.BIBLE_API_KEY; const PUSHCUT_KEY = process.env.PUSHCUT_KEY; const PROTON_USER = process.env.PROTON_USER; const PROTON_PASS = process.env.PROTON_PASS; const PROTON_BRIDGE = process.env.PROTON_BRIDGE || '127.0.0.1'; // Session storage with expiration let abodeSession = { token: null, expires: 0 }; let unifiSession = { cookies: [], token: null, expires: 0 }; // ============ SECURITY HELPERS ============ function timingSafeEqual(a, b) { if (!a || !b) return false; const bufA = Buffer.from(a); const bufB = Buffer.from(b); if (bufA.length !== bufB.length) { // Compare against self to maintain constant time crypto.timingSafeEqual(bufA, bufA); return false; } return crypto.timingSafeEqual(bufA, bufB); } function checkRateLimit(ip) { const now = Date.now(); const record = rateLimitMap.get(ip) || { count: 0, resetAt: now + RATE_LIMIT_WINDOW }; if (now > record.resetAt) { record.count = 1; record.resetAt = now + RATE_LIMIT_WINDOW; } else { record.count++; } rateLimitMap.set(ip, record); // Cleanup old entries every 100 requests if (rateLimitMap.size > 1000) { for (const [key, val] of rateLimitMap) { if (now > val.resetAt) rateLimitMap.delete(key); } } return record.count <= RATE_LIMIT_MAX; } function logAudit(action, details) { const entry = { timestamp: new Date().toISOString(), action, ...details }; auditLog.push(entry); if (auditLog.length > MAX_AUDIT_LOG) auditLog.shift(); console.log(`[AUDIT] ${entry.timestamp} ${action}`, JSON.stringify(details)); } function getClientIP(req) { return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.socket?.remoteAddress || 'unknown'; } function sanitizeInput(obj, maxDepth = 5, currentDepth = 0) { if (currentDepth > maxDepth) return '[MAX_DEPTH]'; if (obj === null || obj === undefined) return obj; if (typeof obj === 'string') { // Limit string length and remove control characters return obj.slice(0, 10000).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); } if (typeof obj === 'number' || typeof obj === 'boolean') return obj; if (Array.isArray(obj)) { return obj.slice(0, 100).map(item => sanitizeInput(item, maxDepth, currentDepth + 1)); } if (typeof obj === 'object') { const sanitized = {}; const keys = Object.keys(obj).slice(0, 50); for (const key of keys) { const safeKey = String(key).slice(0, 100); sanitized[safeKey] = sanitizeInput(obj[key], maxDepth, currentDepth + 1); } return sanitized; } return String(obj).slice(0, 1000); } // ============ HELPERS ============ function jsonFetch(url, options = {}) { return new Promise((resolve, reject) => { const parsed = new URL(url); const isHttps = parsed.protocol === 'https:'; const lib = isHttps ? https : http; const reqOptions = { hostname: parsed.hostname, port: parsed.port || (isHttps ? 443 : 80), path: parsed.pathname + parsed.search, method: options.method || 'GET', headers: { 'Content-Type': 'application/json', ...options.headers }, rejectUnauthorized: false, timeout: options.timeout || 15000 }; const req = lib.request(reqOptions, (res) => { const chunks = []; res.on('data', chunk => chunks.push(chunk)); res.on('end', () => { const body = Buffer.concat(chunks).toString(); try { resolve({ status: res.statusCode, headers: res.headers, data: JSON.parse(body) }); } catch { resolve({ status: res.statusCode, headers: res.headers, data: body }); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); }); if (options.body) req.write(typeof options.body === 'string' ? options.body : JSON.stringify(options.body)); req.end(); }); } // ============ BEEPER ============ async function beeperRequest(path, method = 'GET', body = null) { if (!BEEPER_TOKEN) throw new Error('BEEPER_TOKEN not configured'); const res = await jsonFetch(`https://api.beeper.com/v1${path}`, { method, headers: { 'Authorization': `Bearer ${BEEPER_TOKEN}` }, body }); if (res.status >= 400) throw new Error(`Beeper error: ${res.status}`); return res.data; } const beeperTools = { async beeper_get_accounts() { return await beeperRequest('/accounts'); }, async beeper_search(args) { return await beeperRequest(`/search?q=${encodeURIComponent(args.query)}`); }, async beeper_search_chats(args) { let url = '/chats?'; if (args.query) url += `q=${encodeURIComponent(args.query)}&`; if (args.scope) url += `scope=${args.scope}&`; if (args.type) url += `type=${args.type}&`; if (args.limit) url += `limit=${args.limit}&`; if (args.unreadOnly) url += `unread_only=true&`; return await beeperRequest(url); }, async beeper_search_messages(args) { let url = '/messages/search?'; if (args.query) url += `q=${encodeURIComponent(args.query)}&`; if (args.chatIDs) url += `chat_ids=${args.chatIDs.join(',')}&`; if (args.limit) url += `limit=${args.limit}&`; if (args.dateAfter) url += `date_after=${args.dateAfter}&`; if (args.dateBefore) url += `date_before=${args.dateBefore}&`; return await beeperRequest(url); }, async beeper_get_chat(args) { return await beeperRequest(`/chats/${args.chatID}`); }, async beeper_list_messages(args) { let url = `/chats/${args.chatID}/messages`; if (args.cursor) url += `?cursor=${args.cursor}`; return await beeperRequest(url); }, async beeper_send_message(args) { return await beeperRequest(`/chats/${args.chatID}/messages`, 'POST', { text: args.text }); }, async beeper_archive_chat(args) { return await beeperRequest(`/chats/${args.chatID}/archive`, 'POST', { archived: args.archived !== false }); }, async beeper_set_chat_reminder(args) { return await beeperRequest(`/chats/${args.chatID}/reminder`, 'POST', { remindAtMs: args.remindAtMs, dismissOnIncomingMessage: args.dismissOnIncomingMessage }); }, async beeper_clear_chat_reminder(args) { return await beeperRequest(`/chats/${args.chatID}/reminder`, 'DELETE'); } }; // ============ ABODE ============ async function abodeAuth() { const now = Date.now(); if (abodeSession.token && now < abodeSession.expires) return abodeSession.token; if (!ABODE_USER || !ABODE_PASS) throw new Error('Abode credentials not configured'); const res = await jsonFetch('https://my.goabode.com/api/auth2/login', { method: 'POST', body: { id: ABODE_USER, password: ABODE_PASS } }); if (res.status !== 200) throw new Error('Abode auth failed'); abodeSession = { token: res.data.token, expires: now + SESSION_EXPIRY }; logAudit('abode_auth', { success: true }); return abodeSession.token; } async function abodeRequest(path, method = 'GET', body = null) { const token = await abodeAuth(); const res = await jsonFetch(`https://my.goabode.com/api${path}`, { method, headers: { 'Authorization': `Bearer ${token}` }, body }); if (res.status >= 400) throw new Error(`Abode error: ${res.status}`); return res.data; } const abodeTools = { async abode_get_mode() { const panel = await abodeRequest('/v1/panel'); return { mode: panel.mode }; }, async abode_set_mode(args) { logAudit('abode_set_mode', { mode: args.mode }); return await abodeRequest('/v1/panel/mode', 'PUT', { mode: args.mode }); }, async abode_list_devices() { return await abodeRequest('/v1/devices'); }, async abode_get_device(args) { return await abodeRequest(`/v1/devices/${args.device_id}`); }, async abode_lock_device(args) { logAudit('abode_lock_device', { device_id: args.device_id, lock: args.lock }); return await abodeRequest(`/v1/devices/${args.device_id}/lock`, 'PUT', { lock: args.lock }); }, async abode_switch_device(args) { logAudit('abode_switch_device', { device_id: args.device_id, on: args.on }); return await abodeRequest(`/v1/devices/${args.device_id}/switch`, 'PUT', { on: args.on }); }, async abode_get_settings() { return await abodeRequest('/v1/panel'); }, async abode_list_automations() { return await abodeRequest('/v1/automations'); }, async abode_trigger_automation(args) { logAudit('abode_trigger_automation', { automation_id: args.automation_id }); return await abodeRequest(`/v1/automations/${args.automation_id}/trigger`, 'POST'); } }; // ============ UNIFI PROTECT ============ async function unifiAuth() { const now = Date.now(); if (unifiSession.token && now < unifiSession.expires) return; if (!UNIFI_USER || !UNIFI_PASS) throw new Error('UniFi credentials not configured'); const res = await jsonFetch(`https://${UNIFI_HOST}/api/auth/login`, { method: 'POST', body: { username: UNIFI_USER, password: UNIFI_PASS, rememberMe: true } }); if (res.status !== 200) throw new Error('UniFi auth failed'); const setCookies = res.headers['set-cookie'] || []; unifiSession.cookies = setCookies.map(c => c.split(';')[0]); const tokenCookie = unifiSession.cookies.find(c => c.startsWith('TOKEN=')); if (tokenCookie) unifiSession.token = tokenCookie.split('=')[1]; unifiSession.expires = now + SESSION_EXPIRY; logAudit('unifi_auth', { success: true, host: UNIFI_HOST }); } async function unifiRequest(path, method = 'GET', body = null) { await unifiAuth(); const res = await jsonFetch(`https://${UNIFI_HOST}/proxy/protect/api${path}`, { method, headers: { 'Cookie': unifiSession.cookies.join('; ') }, body }); if (res.status >= 400) throw new Error(`UniFi error: ${res.status}`); return res.data; } const unifiTools = { async unifi_system_info() { const bootstrap = await unifiRequest('/bootstrap'); return { version: bootstrap.nvr?.version, name: bootstrap.nvr?.name, uptime: bootstrap.nvr?.uptime, cameras: bootstrap.cameras?.length || 0, lights: bootstrap.lights?.length || 0, sensors: bootstrap.sensors?.length || 0 }; }, async unifi_list_cameras() { const cameras = await unifiRequest('/cameras'); return cameras.map(c => ({ id: c.id, name: c.name, type: c.type, state: c.state, isConnected: c.isConnected })); }, async unifi_get_camera(args) { return await unifiRequest(`/cameras/${args.camera_id}`); }, async unifi_get_snapshot(args) { await unifiAuth(); return new Promise((resolve, reject) => { const req = https.request({ hostname: UNIFI_HOST, port: 443, path: `/proxy/protect/api/cameras/${args.camera_id}/snapshot?ts=${Date.now()}&force=true`, method: 'GET', headers: { 'Cookie': unifiSession.cookies.join('; ') }, rejectUnauthorized: false, timeout: 8000 }, (res) => { const chunks = []; res.on('data', chunk => chunks.push(chunk)); res.on('end', () => { if (res.statusCode === 200) { const buffer = Buffer.concat(chunks); resolve({ camera_id: args.camera_id, size: buffer.length, base64: buffer.toString('base64') }); } else { reject(new Error(`Snapshot failed: ${res.statusCode}`)); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); }); req.end(); }); }, async unifi_get_events(args) { const now = Date.now(); const start = now - (args.minutes_ago || 30) * 60000; let events = await unifiRequest(`/events?start=${start}&end=${now}`); if (args.limit) events = events.slice(0, args.limit); return events; }, async unifi_list_sensors() { return await unifiRequest('/sensors'); }, async unifi_list_lights() { return await unifiRequest('/lights'); }, async unifi_set_light(args) { logAudit('unifi_set_light', { light_id: args.light_id, on: args.on }); const body = { lightOnSettings: { isLedForceOn: args.on } }; if (args.brightness !== undefined) body.lightDeviceSettings = { ledLevel: args.brightness }; return await unifiRequest(`/lights/${args.light_id}`, 'PATCH', body); }, async unifi_list_chimes() { return await unifiRequest('/chimes'); }, async unifi_play_chime(args) { logAudit('unifi_play_chime', { chime_id: args.chime_id }); return await unifiRequest(`/chimes/${args.chime_id}/play`, 'POST', { volume: args.volume || 50 }); }, async unifi_list_liveviews() { return await unifiRequest('/liveviews'); }, async unifi_list_viewers() { return await unifiRequest('/viewers'); }, async unifi_ptz_move(args) { return await unifiRequest(`/cameras/${args.camera_id}/ptz/relative`, 'POST', { pan: args.pan || 0, tilt: args.tilt || 0, zoom: args.zoom || 0 }); }, async unifi_ptz_goto_preset(args) { return await unifiRequest(`/cameras/${args.camera_id}/ptz/goto/${args.slot}`, 'POST'); }, async unifi_set_camera_led(args) { logAudit('unifi_set_camera_led', { camera_id: args.camera_id, enabled: args.enabled }); return await unifiRequest(`/cameras/${args.camera_id}`, 'PATCH', { ledSettings: { isEnabled: args.enabled } }); }, async unifi_set_camera_mic(args) { logAudit('unifi_set_camera_mic', { camera_id: args.camera_id, enabled: args.enabled }); return await unifiRequest(`/cameras/${args.camera_id}`, 'PATCH', { isMicEnabled: args.enabled }); }, async unifi_set_lcd_message(args) { logAudit('unifi_set_lcd_message', { camera_id: args.camera_id, message: args.message?.slice(0, 50) }); return await unifiRequest(`/cameras/${args.camera_id}`, 'PATCH', { lcdMessage: { type: 'CUSTOM_MESSAGE', text: args.message, resetAt: args.duration ? Date.now() + args.duration : null } }); }, async unifi_health_check() { await unifiAuth(); return { status: 'healthy', host: UNIFI_HOST, authenticated: !!unifiSession.token }; } }; // ============ GRAPHITI ============ const graphitiTools = { async graphiti_search(args) { if (!GRAPHITI_URL) throw new Error('GRAPHITI_URL not configured'); const res = await jsonFetch(`${GRAPHITI_URL}/search`, { method: 'POST', body: { query: args.query, limit: args.limit || 10 } }); return res.data; }, async graphiti_get_facts(args) { if (!GRAPHITI_URL) throw new Error('GRAPHITI_URL not configured'); const url = args.entity ? `${GRAPHITI_URL}/facts?entity=${encodeURIComponent(args.entity)}` : `${GRAPHITI_URL}/facts`; const res = await jsonFetch(url); return res.data; }, async graphiti_add_episode(args) { if (!GRAPHITI_URL) throw new Error('GRAPHITI_URL not configured'); logAudit('graphiti_add_episode', { name: args.name }); const res = await jsonFetch(`${GRAPHITI_URL}/episodes`, { method: 'POST', body: { name: args.name, content: args.content, source: args.source || 'claude' } }); return res.data; } }; // ============ BIBLE ============ const bibleTools = { async bible_votd() { if (!BIBLE_API_KEY) throw new Error('BIBLE_API_KEY not configured'); const res = await jsonFetch(`https://api.scripture.api.bible/v1/bibles/de4e12af7f28f599-01/verses-of-day`, { headers: { 'api-key': BIBLE_API_KEY } }); return res.data; }, async bible_passage(args) { if (!BIBLE_API_KEY) throw new Error('BIBLE_API_KEY not configured'); const res = await jsonFetch(`https://api.scripture.api.bible/v1/bibles/de4e12af7f28f599-01/passages/${encodeURIComponent(args.reference)}`, { headers: { 'api-key': BIBLE_API_KEY } }); return res.data; }, async bible_search(args) { if (!BIBLE_API_KEY) throw new Error('BIBLE_API_KEY not configured'); const res = await jsonFetch(`https://api.scripture.api.bible/v1/bibles/de4e12af7f28f599-01/search?query=${encodeURIComponent(args.query)}`, { headers: { 'api-key': BIBLE_API_KEY } }); return res.data; } }; // ============ PROTONMAIL (IMAP) ============ const Imap = require('imap'); const { simpleParser } = require('mailparser'); const nodemailer = require('nodemailer'); function getImapClient() { if (!PROTON_USER || !PROTON_PASS) throw new Error('ProtonMail credentials not configured'); return new Imap({ user: PROTON_USER, password: PROTON_PASS, host: PROTON_BRIDGE, port: 1143, tls: false, autotls: 'never', connTimeout: 10000 }); } const protonTools = { async search_protonmail(args) { return new Promise((resolve, reject) => { const imap = getImapClient(); const results = []; imap.once('ready', () => { imap.openBox('INBOX', true, (err) => { if (err) { imap.end(); return reject(err); } const criteria = args.criteria ? args.criteria.split(' ') : ['ALL']; imap.search(criteria, (err, uids) => { if (err) { imap.end(); return reject(err); } const limited = uids.slice(-(args.limit || 10)); if (limited.length === 0) { imap.end(); return resolve([]); } const fetch = imap.fetch(limited, { bodies: 'HEADER.FIELDS (FROM TO SUBJECT DATE)', struct: true }); fetch.on('message', (msg, seqno) => { let uid, header; msg.on('attributes', (attrs) => { uid = attrs.uid; }); msg.on('body', (stream) => { let buffer = ''; stream.on('data', (chunk) => buffer += chunk); stream.on('end', () => { header = buffer; }); }); msg.once('end', () => { results.push({ uid, header }); }); }); fetch.once('end', () => { imap.end(); resolve(results); }); }); }); }); imap.once('error', reject); imap.connect(); }); }, async read_protonmail(args) { return new Promise((resolve, reject) => { const imap = getImapClient(); imap.once('ready', () => { imap.openBox('INBOX', true, (err) => { if (err) { imap.end(); return reject(err); } const fetch = imap.fetch([args.uid], { bodies: '' }); fetch.on('message', (msg) => { msg.on('body', (stream) => { simpleParser(stream, (err, parsed) => { imap.end(); if (err) return reject(err); resolve({ uid: args.uid, from: parsed.from?.text, to: parsed.to?.text, subject: parsed.subject, date: parsed.date, text: parsed.text, html: parsed.html }); }); }); }); }); }); imap.once('error', reject); imap.connect(); }); }, async send_protonmail(args) { if (!PROTON_USER || !PROTON_PASS) throw new Error('ProtonMail credentials not configured'); logAudit('send_protonmail', { to: args.to, subject: args.subject?.slice(0, 50) }); const transporter = nodemailer.createTransport({ host: PROTON_BRIDGE, port: 1025, secure: false, auth: { user: PROTON_USER, pass: PROTON_PASS } }); const result = await transporter.sendMail({ from: PROTON_USER, to: args.to, cc: args.cc, bcc: args.bcc, subject: args.subject, text: args.body }); return { messageId: result.messageId, accepted: result.accepted }; }, async list_protonmail_folders() { return new Promise((resolve, reject) => { const imap = getImapClient(); imap.once('ready', () => { imap.getBoxes((err, boxes) => { imap.end(); if (err) return reject(err); resolve(boxes); }); }); imap.once('error', reject); imap.connect(); }); } }; // ============ PUSHCUT ============ const pushTools = { async push_notify(args) { if (!PUSHCUT_KEY) throw new Error('PUSHCUT_KEY not configured'); logAudit('push_notify', { title: args.title }); const res = await jsonFetch(`https://api.pushcut.io/${PUSHCUT_KEY}/notifications/${encodeURIComponent(args.title)}`, { method: 'POST', body: { text: args.text, sound: args.sound, isTimeSensitive: args.isTimeSensitive } }); return res.data; } }; // ============ ADMIN TOOLS ============ const adminTools = { async get_audit_log(args) { const limit = Math.min(args.limit || 50, 200); return auditLog.slice(-limit); }, async get_rate_limits() { const now = Date.now(); const active = []; for (const [ip, record] of rateLimitMap) { if (now < record.resetAt) { active.push({ ip: ip.slice(0, 20) + '...', count: record.count, resetIn: Math.round((record.resetAt - now) / 1000) }); } } return { active_limits: active.length, limits: active.slice(0, 20) }; }, async clear_sessions() { abodeSession = { token: null, expires: 0 }; unifiSession = { cookies: [], token: null, expires: 0 }; logAudit('clear_sessions', { success: true }); return { cleared: true }; } }; // ============ TOOL REGISTRY ============ const ALL_TOOLS = { ...beeperTools, ...abodeTools, ...unifiTools, ...graphitiTools, ...bibleTools, ...protonTools, ...pushTools, ping() { return { status: 'pong', timestamp: new Date().toISOString(), version: VERSION }; } }; // Admin tools require admin key const ADMIN_TOOL_NAMES = ['get_audit_log', 'get_rate_limits', 'clear_sessions']; const TOOL_SCHEMAS = [ // Beeper { name: 'beeper_get_accounts', description: 'List connected messaging accounts', inputSchema: { type: 'object', properties: {} } }, { name: 'beeper_search', description: 'Search chats and messages', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } }, { name: 'beeper_search_chats', description: 'Search chats by title or participants', inputSchema: { type: 'object', properties: { query: { type: 'string' }, scope: { type: 'string', enum: ['titles', 'participants'] }, type: { type: 'string', enum: ['single', 'group', 'any'] }, limit: { type: 'number' }, unreadOnly: { type: 'boolean' } } } }, { name: 'beeper_search_messages', description: 'Search messages across chats', inputSchema: { type: 'object', properties: { query: { type: 'string' }, chatIDs: { type: 'array', items: { type: 'string' } }, limit: { type: 'number' }, dateAfter: { type: 'string' }, dateBefore: { type: 'string' } } } }, { name: 'beeper_get_chat', description: 'Get chat details', inputSchema: { type: 'object', properties: { chatID: { type: 'string' } }, required: ['chatID'] } }, { name: 'beeper_list_messages', description: 'List messages from a chat', inputSchema: { type: 'object', properties: { chatID: { type: 'string' }, cursor: { type: 'string' } }, required: ['chatID'] } }, { name: 'beeper_send_message', description: 'Send a message', inputSchema: { type: 'object', properties: { chatID: { type: 'string' }, text: { type: 'string' } }, required: ['chatID', 'text'] } }, { name: 'beeper_archive_chat', description: 'Archive/unarchive a chat', inputSchema: { type: 'object', properties: { chatID: { type: 'string' }, archived: { type: 'boolean' } }, required: ['chatID'] } }, { name: 'beeper_set_chat_reminder', description: 'Set a chat reminder', inputSchema: { type: 'object', properties: { chatID: { type: 'string' }, remindAtMs: { type: 'number' }, dismissOnIncomingMessage: { type: 'boolean' } }, required: ['chatID', 'remindAtMs'] } }, { name: 'beeper_clear_chat_reminder', description: 'Clear a chat reminder', inputSchema: { type: 'object', properties: { chatID: { type: 'string' } }, required: ['chatID'] } }, // Abode { name: 'abode_get_mode', description: 'Get current alarm mode', inputSchema: { type: 'object', properties: {} } }, { name: 'abode_set_mode', description: 'Set alarm mode', inputSchema: { type: 'object', properties: { mode: { type: 'string', enum: ['standby', 'home', 'away'] } }, required: ['mode'] } }, { name: 'abode_list_devices', description: 'List all devices', inputSchema: { type: 'object', properties: {} } }, { name: 'abode_get_device', description: 'Get device details', inputSchema: { type: 'object', properties: { device_id: { type: 'string' } }, required: ['device_id'] } }, { name: 'abode_lock_device', description: 'Lock/unlock device', inputSchema: { type: 'object', properties: { device_id: { type: 'string' }, lock: { type: 'boolean' } }, required: ['device_id', 'lock'] } }, { name: 'abode_switch_device', description: 'Switch device on/off', inputSchema: { type: 'object', properties: { device_id: { type: 'string' }, on: { type: 'boolean' } }, required: ['device_id', 'on'] } }, { name: 'abode_get_settings', description: 'Get panel settings', inputSchema: { type: 'object', properties: {} } }, { name: 'abode_list_automations', description: 'List automations', inputSchema: { type: 'object', properties: {} } }, { name: 'abode_trigger_automation', description: 'Trigger automation', inputSchema: { type: 'object', properties: { automation_id: { type: 'string' } }, required: ['automation_id'] } }, // UniFi { name: 'unifi_system_info', description: 'Get system info', inputSchema: { type: 'object', properties: {} } }, { name: 'unifi_list_cameras', description: 'List all cameras', inputSchema: { type: 'object', properties: {} } }, { name: 'unifi_get_camera', description: 'Get camera details', inputSchema: { type: 'object', properties: { camera_id: { type: 'string' } }, required: ['camera_id'] } }, { name: 'unifi_get_snapshot', description: 'Get camera snapshot', inputSchema: { type: 'object', properties: { camera_id: { type: 'string' } }, required: ['camera_id'] } }, { name: 'unifi_get_events', description: 'Get motion events', inputSchema: { type: 'object', properties: { minutes_ago: { type: 'number' }, limit: { type: 'number' } } } }, { name: 'unifi_list_sensors', description: 'List sensors', inputSchema: { type: 'object', properties: {} } }, { name: 'unifi_list_lights', description: 'List lights', inputSchema: { type: 'object', properties: {} } }, { name: 'unifi_set_light', description: 'Control light', inputSchema: { type: 'object', properties: { light_id: { type: 'string' }, on: { type: 'boolean' }, brightness: { type: 'number' } }, required: ['light_id'] } }, { name: 'unifi_list_chimes', description: 'List chimes', inputSchema: { type: 'object', properties: {} } }, { name: 'unifi_play_chime', description: 'Play chime', inputSchema: { type: 'object', properties: { chime_id: { type: 'string' }, volume: { type: 'number' } }, required: ['chime_id'] } }, { name: 'unifi_list_liveviews', description: 'List liveviews', inputSchema: { type: 'object', properties: {} } }, { name: 'unifi_list_viewers', description: 'List viewers', inputSchema: { type: 'object', properties: {} } }, { name: 'unifi_ptz_move', description: 'PTZ control', inputSchema: { type: 'object', properties: { camera_id: { type: 'string' }, pan: { type: 'number' }, tilt: { type: 'number' }, zoom: { type: 'number' } }, required: ['camera_id'] } }, { name: 'unifi_ptz_goto_preset', description: 'PTZ preset', inputSchema: { type: 'object', properties: { camera_id: { type: 'string' }, slot: { type: 'number' } }, required: ['camera_id', 'slot'] } }, { name: 'unifi_set_camera_led', description: 'Set camera LED', inputSchema: { type: 'object', properties: { camera_id: { type: 'string' }, enabled: { type: 'boolean' } }, required: ['camera_id', 'enabled'] } }, { name: 'unifi_set_camera_mic', description: 'Set camera mic', inputSchema: { type: 'object', properties: { camera_id: { type: 'string' }, enabled: { type: 'boolean' } }, required: ['camera_id', 'enabled'] } }, { name: 'unifi_set_lcd_message', description: 'Set doorbell LCD', inputSchema: { type: 'object', properties: { camera_id: { type: 'string' }, message: { type: 'string' }, duration: { type: 'number' } }, required: ['camera_id', 'message'] } }, { name: 'unifi_health_check', description: 'Health check', inputSchema: { type: 'object', properties: {} } }, // Graphiti { name: 'graphiti_search', description: 'Search knowledge graph', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' } }, required: ['query'] } }, { name: 'graphiti_get_facts', description: 'Get facts', inputSchema: { type: 'object', properties: { entity: { type: 'string' } } } }, { name: 'graphiti_add_episode', description: 'Add episode', inputSchema: { type: 'object', properties: { name: { type: 'string' }, content: { type: 'string' }, source: { type: 'string' } }, required: ['name', 'content'] } }, // Bible { name: 'bible_votd', description: 'Verse of the day', inputSchema: { type: 'object', properties: {} } }, { name: 'bible_passage', description: 'Get passage', inputSchema: { type: 'object', properties: { reference: { type: 'string' } }, required: ['reference'] } }, { name: 'bible_search', description: 'Search Bible', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } }, // ProtonMail { name: 'search_protonmail', description: 'Search inbox', inputSchema: { type: 'object', properties: { criteria: { type: 'string' }, limit: { type: 'number' } } } }, { name: 'read_protonmail', description: 'Read email by UID', inputSchema: { type: 'object', properties: { uid: { type: 'number' } }, required: ['uid'] } }, { name: 'send_protonmail', description: 'Send email', inputSchema: { type: 'object', properties: { to: { type: 'string' }, subject: { type: 'string' }, body: { type: 'string' }, cc: { type: 'string' }, bcc: { type: 'string' } }, required: ['to', 'subject', 'body'] } }, { name: 'list_protonmail_folders', description: 'List folders', inputSchema: { type: 'object', properties: {} } }, // Push { name: 'push_notify', description: 'Send push notification', inputSchema: { type: 'object', properties: { title: { type: 'string' }, text: { type: 'string' }, sound: { type: 'string' }, isTimeSensitive: { type: 'boolean' } }, required: ['title', 'text'] } }, // System { name: 'ping', description: 'Health check', inputSchema: { type: 'object', properties: {} } }, // Admin (requires admin key) { name: 'get_audit_log', description: '[ADMIN] Get audit log', inputSchema: { type: 'object', properties: { limit: { type: 'number' } } } }, { name: 'get_rate_limits', description: '[ADMIN] Get rate limit status', inputSchema: { type: 'object', properties: {} } }, { name: 'clear_sessions', description: '[ADMIN] Clear all cached sessions', inputSchema: { type: 'object', properties: {} } } ]; // ============ HTTP SERVER ============ const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); const path = url.pathname; const clientIP = getClientIP(req); const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key' }; if (req.method === 'OPTIONS') { res.writeHead(204, cors); return res.end(); } // Health check (no auth, no rate limit) if (path === '/health' || path === '/') { res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ status: 'healthy', version: VERSION, tools: TOOL_SCHEMAS.length })); } // Rate limit check if (!checkRateLimit(clientIP)) { logAudit('rate_limit_exceeded', { ip: clientIP }); res.writeHead(429, cors); return res.end(JSON.stringify({ error: 'Rate limit exceeded. Try again later.' })); } // Auth check with timing-safe comparison const providedKey = url.searchParams.get('key') || req.headers['x-api-key']; const isValidKey = timingSafeEqual(providedKey, API_KEY); const isAdminKey = ADMIN_KEY && timingSafeEqual(providedKey, ADMIN_KEY); if (!isValidKey && !isAdminKey) { logAudit('auth_failed', { ip: clientIP, path }); res.writeHead(401, cors); return res.end(JSON.stringify({ error: 'Unauthorized' })); } // MCP endpoint if (path === '/sse' || path === '/mcp') { if (req.method !== 'POST') { res.writeHead(405, cors); return res.end(JSON.stringify({ error: 'POST required' })); } let body = ''; req.on('data', chunk => body += chunk); req.on('end', async () => { try { const parsed = JSON.parse(body); const { method, params, id } = sanitizeInput(parsed); let result; if (method === 'initialize') { result = { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'garza-home-mcp', version: VERSION } }; } else if (method === 'tools/list') { // Filter admin tools if not using admin key const tools = isAdminKey ? TOOL_SCHEMAS : TOOL_SCHEMAS.filter(t => !ADMIN_TOOL_NAMES.includes(t.name)); result = { tools }; } else if (method === 'tools/call') { const toolName = params.name; // Check admin tool access if (ADMIN_TOOL_NAMES.includes(toolName)) { if (!isAdminKey) { throw new Error(`Tool ${toolName} requires admin key`); } const toolFn = adminTools[toolName]; if (!toolFn) throw new Error(`Unknown admin tool: ${toolName}`); const toolResult = await toolFn(sanitizeInput(params.arguments || {})); result = { content: [{ type: 'text', text: JSON.stringify(toolResult, null, 2) }] }; } else { const toolFn = ALL_TOOLS[toolName]; if (!toolFn) throw new Error(`Unknown tool: ${toolName}`); logAudit('tool_call', { ip: clientIP, tool: toolName }); const toolResult = await toolFn(sanitizeInput(params.arguments || {})); result = { content: [{ type: 'text', text: JSON.stringify(toolResult, null, 2) }] }; } } else { throw new Error(`Unknown method: ${method}`); } res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id, result })); } catch (e) { logAudit('tool_error', { ip: clientIP, error: e.message }); res.writeHead(500, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: e.message } })); } }); return; } res.writeHead(404, cors); res.end(JSON.stringify({ error: 'Not found' })); }); server.listen(PORT, () => { console.log(`Garza Home MCP v${VERSION} (SECURED) listening on port ${PORT}`); console.log(`Tools: ${TOOL_SCHEMAS.length} (${ADMIN_TOOL_NAMES.length} admin)`); console.log(`Rate limit: ${RATE_LIMIT_MAX} req/${RATE_LIMIT_WINDOW/1000}s`); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/itsablabla/garza-home-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server