Skip to main content
Glama

Nanoleaf MCP Server

by srnetadmin
nanoleaf-client.ts6.3 kB
import axios, { AxiosInstance } from 'axios'; import https from 'https'; import pkg from 'node-ssdp'; const { Client } = pkg; export interface NanoleafDevice { ip: string; port: number; protocol?: string; authToken?: string; } export interface NanoleafInfo { name: string; serialNo: string; manufacturer: string; firmwareVersion: string; model: string; state: { on: { value: boolean; }; brightness: { value: number; max: number; min: number; }; hue: { value: number; max: number; min: number; }; sat: { value: number; max: number; min: number; }; ct: { value: number; max: number; min: number; }; colorMode: string; }; effects: { select: string; effectsList: string[]; }; panelLayout: any; } export class NanoleafClient { private device: NanoleafDevice; private httpClient: AxiosInstance; constructor(device: NanoleafDevice) { this.device = device; this.httpClient = axios.create({ baseURL: `${device.protocol || 'http'}://${device.ip}:${device.port}/api/v1`, timeout: 5000, // Ignore SSL certificate errors for self-signed certs when using HTTPS httpsAgent: device.protocol === 'https' ? new https.Agent({ rejectUnauthorized: false }) : undefined }); } static async discover(): Promise<NanoleafDevice[]> { return new Promise((resolve, reject) => { const client = new Client(); const devices: NanoleafDevice[] = []; const timeout = setTimeout(() => { client.stop(); resolve(devices); }, 5000); client.on('response', (headers: any, statusCode: number, rinfo: any) => { if (headers.ST && headers.ST.includes('nanoleaf')) { const location = headers.LOCATION; if (location) { const url = new URL(location); devices.push({ ip: url.hostname, port: parseInt(url.port) || 16021, }); } } }); client.search('upnp:rootdevice'); }); } static async connectToIP(ip: string, port: number = 16021): Promise<NanoleafDevice | null> { // Try different protocols and ports const attempts = [ { ip, port, protocol: 'http' }, { ip, port: 80, protocol: 'http' }, { ip, port: 443, protocol: 'https' }, { ip, port, protocol: 'https' } ]; for (const attempt of attempts) { try { const httpClient = axios.create({ baseURL: `${attempt.protocol}://${attempt.ip}:${attempt.port}/api/v1`, timeout: 5000, // Ignore SSL certificate errors for self-signed certs httpsAgent: attempt.protocol === 'https' ? new https.Agent({ rejectUnauthorized: false }) : undefined }); // Try to make a basic request to see if it's a Nanoleaf device const response = await httpClient.get('/'); // If we get here without error, it's likely a Nanoleaf device return { ip: attempt.ip, port: attempt.port, protocol: attempt.protocol }; } catch (error: any) { console.error(`Attempt ${attempt.protocol}://${attempt.ip}:${attempt.port} failed:`, error.message); // Check if it's a 403 Forbidden (typical for Nanoleaf without auth) if (error.response && error.response.status === 403) { console.error(`Found Nanoleaf device at ${attempt.protocol}://${attempt.ip}:${attempt.port} (403 Forbidden - needs auth)`); return { ip: attempt.ip, port: attempt.port, protocol: attempt.protocol }; } // Continue to next attempt if other error continue; } } return null; } async authorize(): Promise<string> { try { const response = await this.httpClient.post('/new'); const authToken = response.data.auth_token; this.device.authToken = authToken; return authToken; } catch (error) { throw new Error('Failed to authorize. Make sure to hold the power button on your Nanoleaf device for 5-7 seconds.'); } } private getAuthUrl(endpoint: string): string { if (!this.device.authToken) { throw new Error('Device not authorized. Call authorize() first.'); } return `/${this.device.authToken}${endpoint}`; } async getInfo(): Promise<NanoleafInfo> { const response = await this.httpClient.get(this.getAuthUrl('')); return response.data; } async turnOn(): Promise<void> { await this.httpClient.put(this.getAuthUrl('/state'), { on: { value: true } }); } async turnOff(): Promise<void> { await this.httpClient.put(this.getAuthUrl('/state'), { on: { value: false } }); } async setBrightness(brightness: number): Promise<void> { if (brightness < 0 || brightness > 100) { throw new Error('Brightness must be between 0 and 100'); } await this.httpClient.put(this.getAuthUrl('/state'), { brightness: { value: brightness } }); } async setColor(hue: number, saturation: number): Promise<void> { if (hue < 0 || hue > 360) { throw new Error('Hue must be between 0 and 360'); } if (saturation < 0 || saturation > 100) { throw new Error('Saturation must be between 0 and 100'); } await this.httpClient.put(this.getAuthUrl('/state'), { hue: { value: hue }, sat: { value: saturation } }); } async setEffect(effectName: string): Promise<void> { await this.httpClient.put(this.getAuthUrl('/effects'), { select: effectName }); } async getEffects(): Promise<string[]> { const response = await this.httpClient.get(this.getAuthUrl('/effects/effectsList')); return response.data; } async setColorTemperature(temperature: number): Promise<void> { if (temperature < 1200 || temperature > 6500) { throw new Error('Color temperature must be between 1200K and 6500K'); } await this.httpClient.put(this.getAuthUrl('/state'), { ct: { value: temperature } }); } }

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/srnetadmin/nanoleaf-mcp-server'

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