Skip to main content
Glama

homeassistant-mcp

by tevonsb
index.ts6.05 kB
import crypto from 'crypto'; import { Request, Response, NextFunction } from 'express'; import rateLimit from 'express-rate-limit'; import helmet from 'helmet'; import { HelmetOptions } from 'helmet'; // Security configuration const RATE_LIMIT_WINDOW = 15 * 60 * 1000; // 15 minutes const RATE_LIMIT_MAX = 100; // requests per window const TOKEN_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours // Rate limiting middleware export const rateLimiter = rateLimit({ windowMs: RATE_LIMIT_WINDOW, max: RATE_LIMIT_MAX, message: 'Too many requests from this IP, please try again later' }); // Security configuration const helmetConfig: HelmetOptions = { contentSecurityPolicy: { useDefaults: true, directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', 'https:'], connectSrc: ["'self'", 'wss:', 'https:'] } }, dnsPrefetchControl: true, frameguard: true, hidePoweredBy: true, hsts: true, ieNoOpen: true, noSniff: true, referrerPolicy: { policy: ['no-referrer', 'strict-origin-when-cross-origin'] } }; // Security headers middleware export const securityHeaders = helmet(helmetConfig); // Token validation and encryption export class TokenManager { private static readonly algorithm = 'aes-256-gcm'; private static readonly keyLength = 32; private static readonly ivLength = 16; private static readonly saltLength = 64; private static readonly tagLength = 16; private static readonly iterations = 100000; private static readonly digest = 'sha512'; private static deriveKey(password: string, salt: Buffer): Buffer { return crypto.pbkdf2Sync( password, salt, this.iterations, this.keyLength, this.digest ); } public static encryptToken(token: string, encryptionKey: string): string { const iv = crypto.randomBytes(this.ivLength); const salt = crypto.randomBytes(this.saltLength); const key = this.deriveKey(encryptionKey, salt); const cipher = crypto.createCipheriv(this.algorithm, key, iv, { authTagLength: this.tagLength }); const encrypted = Buffer.concat([ cipher.update(token, 'utf8'), cipher.final() ]); const tag = cipher.getAuthTag(); return Buffer.concat([salt, iv, tag, encrypted]).toString('base64'); } public static decryptToken(encryptedToken: string, encryptionKey: string): string { const buffer = Buffer.from(encryptedToken, 'base64'); const salt = buffer.subarray(0, this.saltLength); const iv = buffer.subarray(this.saltLength, this.saltLength + this.ivLength); const tag = buffer.subarray( this.saltLength + this.ivLength, this.saltLength + this.ivLength + this.tagLength ); const encrypted = buffer.subarray(this.saltLength + this.ivLength + this.tagLength); const key = this.deriveKey(encryptionKey, salt); const decipher = crypto.createDecipheriv(this.algorithm, key, iv, { authTagLength: this.tagLength }); decipher.setAuthTag(tag); return decipher.update(encrypted) + decipher.final('utf8'); } public static validateToken(token: string): boolean { if (!token) return false; try { // Check token format if (!/^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/.test(token)) { return false; } // Decode token parts const [headerEncoded, payloadEncoded] = token.split('.'); const header = JSON.parse(Buffer.from(headerEncoded, 'base64').toString()); const payload = JSON.parse(Buffer.from(payloadEncoded, 'base64').toString()); // Check token expiry if (payload.exp && Date.now() >= payload.exp * 1000) { return false; } // Additional checks can be added here return true; } catch { return false; } } } // Request validation middleware export function validateRequest(req: Request, res: Response, next: NextFunction) { // Skip validation for health endpoint if (req.path === '/health') { return next(); } // Validate content type if (req.method !== 'GET' && !req.is('application/json')) { return res.status(415).json({ error: 'Unsupported Media Type - Content-Type must be application/json' }); } // Validate token const token = req.headers.authorization?.replace('Bearer ', ''); if (!token || !TokenManager.validateToken(token)) { return res.status(401).json({ error: 'Invalid or expired token' }); } // Validate request body if (req.method !== 'GET' && (!req.body || typeof req.body !== 'object')) { return res.status(400).json({ error: 'Invalid request body' }); } next(); } // Input sanitization middleware export function sanitizeInput(req: Request, res: Response, next: NextFunction) { if (req.body && typeof req.body === 'object') { const sanitized = JSON.parse( JSON.stringify(req.body).replace(/[<>]/g, '') ); req.body = sanitized; } next(); } // Error handling middleware export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) { console.error(err.stack); res.status(500).json({ error: 'Internal Server Error', message: process.env.NODE_ENV === 'development' ? err.message : undefined }); } // Export security middleware chain export const securityMiddleware = [ helmet(helmetConfig), rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }), validateRequest, sanitizeInput, errorHandler ];

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/tevonsb/homeassistant-mcp'

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