Skip to main content
Glama
lights.ts•19.7 kB
import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { HueClient } from '../hue-client.js'; import { LightState } from '../types/index.js'; export function createLightTools(_client: HueClient): Tool[] { return [ { name: 'find_lights', description: 'Smart search for lights by various criteria (name, room, state, capabilities)', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (e.g., "kitchen lights", "all off lights", "color bulbs in bedroom")', }, room: { type: 'string', description: 'Filter by specific room name', }, state: { type: 'string', enum: ['on', 'off', 'reachable', 'unreachable'], description: 'Filter by light state', }, capability: { type: 'string', enum: ['color', 'temperature', 'dimming'], description: 'Filter by light capabilities', }, }, required: ['query'], }, }, { name: 'list_lights', description: 'List all available lights with their current state, with optional filtering', inputSchema: { type: 'object', properties: { filter: { type: 'string', description: 'Filter lights by name, room, or state (e.g., "kitchen", "on", "unreachable")', }, includeRoom: { type: 'boolean', description: 'Include room information for each light', default: true, }, includeCapabilities: { type: 'boolean', description: 'Include detailed capabilities and features', default: false, }, responseSize: { type: 'string', enum: ['compact', 'standard', 'verbose'], description: 'Control response detail level for context efficiency', default: 'standard', }, }, }, }, { name: 'get_light', description: 'Get detailed information about a specific light', inputSchema: { type: 'object', properties: { lightId: { type: 'string', description: 'The ID of the light', }, }, required: ['lightId'], }, }, { name: 'set_light_state', description: 'Control a light - turn on/off, set brightness, color, etc.', inputSchema: { type: 'object', properties: { lightId: { type: 'string', description: 'The ID of the light to control', }, state: { type: 'object', description: 'The desired state for the light', properties: { on: { type: 'boolean', description: 'Whether the light should be on or off', }, brightness: { type: 'number', description: 'Brightness level (0-100)', minimum: 0, maximum: 100, }, hue: { type: 'number', description: 'Hue in degrees (0-360)', minimum: 0, maximum: 360, }, saturation: { type: 'number', description: 'Saturation level (0-100)', minimum: 0, maximum: 100, }, colorTemp: { type: 'number', description: 'Color temperature in mireds (153-500)', minimum: 153, maximum: 500, }, transitionTime: { type: 'number', description: 'Transition time in milliseconds', minimum: 0, }, }, }, naturalLanguage: { type: 'string', description: 'Natural language description of desired state (e.g., "warm white at 50%", "stormy dusk")', }, }, required: ['lightId'], oneOf: [ { required: ['state'] }, { required: ['naturalLanguage'] }, ], }, }, ]; } export async function handleFindLights(client: HueClient, args: any): Promise<any> { try { const allLights = await client.getLights(); const rooms = await client.getRooms(); const query = args.query.toLowerCase(); let matchedLights = allLights; // Apply room filter first if (args.room) { const targetRoom = rooms.find(r => r.name.toLowerCase().includes(args.room.toLowerCase())); if (targetRoom) { matchedLights = matchedLights.filter(light => targetRoom.lights.includes(light.id)); } } // Apply state filter if (args.state) { matchedLights = matchedLights.filter(light => { switch (args.state) { case 'on': return light.state.on; case 'off': return !light.state.on; case 'reachable': return light.state.reachable; case 'unreachable': return !light.state.reachable; default: return true; } }); } // Apply capability filter if (args.capability) { matchedLights = matchedLights.filter(light => { switch (args.capability) { case 'color': return light.capabilities?.control?.colorgamut !== undefined; case 'temperature': return light.capabilities?.control?.ct !== undefined; case 'dimming': return light.capabilities?.control?.mindimlevel !== undefined; default: return true; } }); } // Smart query matching const queryResults = matchedLights.filter(light => { const lightName = light.name.toLowerCase(); const lightType = light.type.toLowerCase(); const room = rooms.find(r => r.lights.includes(light.id)); const roomName = room?.name.toLowerCase() || ''; // Check for exact matches first if (lightName.includes(query) || lightType.includes(query) || roomName.includes(query)) { return true; } // Check for contextual queries const words = query.split(' '); return words.some((word: string) => { return lightName.includes(word) || lightType.includes(word) || roomName.includes(word); }); }); // Add context and suggestions const resultsWithContext = queryResults.map(light => { const room = rooms.find(r => r.lights.includes(light.id)); return { id: light.id, name: light.name, type: light.type, room: room ? { id: room.id, name: room.name } : null, state: { on: light.state.on, brightness: Math.round((light.state.bri / 254) * 100), reachable: light.state.reachable, colorMode: light.state.colormode, }, capabilities: { supportsColor: light.capabilities?.control?.colorgamut !== undefined, supportsColorTemp: light.capabilities?.control?.ct !== undefined, supportsBrightness: light.capabilities?.control?.mindimlevel !== undefined, }, matchReason: getMatchReason(light, query, room), }; }); return { query: args.query, found: resultsWithContext, total: queryResults.length, suggestions: generateSearchSuggestions(queryResults, query, rooms), quickActions: generateBulkActions(queryResults), }; } catch (error) { throw new Error(`Failed to find lights: ${error}`); } } function getMatchReason(light: any, query: string, room: any): string { const lightName = light.name.toLowerCase(); const roomName = room?.name.toLowerCase() || ''; if (lightName.includes(query)) return `Name contains "${query}"`; if (roomName.includes(query)) return `In room "${room.name}"`; if (light.type.toLowerCase().includes(query)) return `Type matches "${query}"`; return 'Partial match'; } function generateSearchSuggestions(lights: any[], query: string, rooms: any[]): string[] { const suggestions: string[] = []; if (lights.length === 0) { suggestions.push(`No lights found for "${query}"`); suggestions.push('Try searching by room name, light name, or "on"/"off"'); // Suggest available rooms const roomNames = rooms.slice(0, 3).map(r => r.name).join(', '); suggestions.push(`Available rooms: ${roomNames}...`); } else if (lights.length > 10) { suggestions.push(`Found ${lights.length} lights - try a more specific search`); } return suggestions; } function generateBulkActions(lights: any[]): any[] { if (lights.length === 0) return []; const actions: any[] = []; const onLights = lights.filter(l => l.state.on); const offLights = lights.filter(l => !l.state.on); if (offLights.length > 0) { actions.push({ action: `Turn on all ${lights.length} found lights`, description: `Turn on ${offLights.length} lights that are currently off`, affectedLights: lights.length, }); } if (onLights.length > 0) { actions.push({ action: `Turn off all ${lights.length} found lights`, description: `Turn off ${onLights.length} lights that are currently on`, affectedLights: lights.length, }); } return actions; } export async function handleListLights(client: HueClient, args: any = {}): Promise<any> { try { const allLights = await client.getLights(); const rooms = args.includeRoom ? await client.getRooms() : []; // Apply filtering let filteredLights = allLights; if (args.filter) { const filterLower = args.filter.toLowerCase(); filteredLights = allLights.filter(light => { return ( light.name.toLowerCase().includes(filterLower) || light.type.toLowerCase().includes(filterLower) || (filterLower === 'on' && light.state.on) || (filterLower === 'off' && !light.state.on) || (filterLower === 'unreachable' && !light.state.reachable) || (filterLower === 'reachable' && light.state.reachable) ); }); } // Helper function to find room for a light const findLightRoom = (lightId: string) => { return rooms.find(room => room.lights.includes(lightId)); }; const lightsWithContext = filteredLights.map(light => { const responseSize = args.responseSize || 'standard'; // Compact response for context efficiency if (responseSize === 'compact') { const compactInfo = { id: light.id, name: light.name, on: light.state.on, brightness: Math.round((light.state.bri / 254) * 100), reachable: light.state.reachable, }; if (args.includeRoom) { const room = findLightRoom(light.id); (compactInfo as any).room = room?.name || null; } return compactInfo; } const baseInfo = { id: light.id, name: light.name, type: responseSize === 'verbose' ? light.type : undefined, modelId: responseSize === 'verbose' ? light.modelid : undefined, manufacturerName: responseSize === 'verbose' ? light.manufacturername : undefined, productName: responseSize === 'verbose' ? light.productname : undefined, state: { on: light.state.on, brightness: Math.round((light.state.bri / 254) * 100), // Convert to 0-100% reachable: light.state.reachable, colorMode: responseSize === 'verbose' ? light.state.colormode : undefined, ...(light.state.hue !== undefined && responseSize !== 'compact' && { hue: Math.round((light.state.hue / 65535) * 360), // Convert to degrees saturation: Math.round((light.state.sat / 254) * 100), // Convert to percentage }), ...(light.state.ct !== undefined && responseSize !== 'compact' && { colorTemp: light.state.ct }), }, }; // Add room context if requested if (args.includeRoom) { const room = findLightRoom(light.id); (baseInfo as any).room = room ? { id: room.id, name: room.name } : null; } // Add capabilities if requested if (args.includeCapabilities) { (baseInfo as any).capabilities = { certified: light.capabilities?.certified, control: light.capabilities?.control, streaming: light.capabilities?.streaming, supportsColor: light.capabilities?.control?.colorgamut !== undefined, supportsColorTemp: light.capabilities?.control?.ct !== undefined, supportsBrightness: light.capabilities?.control?.mindimlevel !== undefined, }; } return baseInfo; }); // Add summary statistics const summary = { totalFound: filteredLights.length, totalLights: allLights.length, onLights: filteredLights.filter(l => l.state.on).length, reachableLights: filteredLights.filter(l => l.state.reachable).length, ...(args.filter && { appliedFilter: args.filter }), }; // Add pagination for large results const maxResults = args.responseSize === 'compact' ? 20 : args.responseSize === 'verbose' ? 5 : 10; const paginatedLights = lightsWithContext.slice(0, maxResults); const hasMore = lightsWithContext.length > maxResults; const result = { lights: paginatedLights, summary: { ...summary, ...(hasMore && { truncated: true, showing: `${maxResults} of ${lightsWithContext.length}`, hint: 'Use more specific filters to see fewer results' }), }, suggestions: generateLightSuggestions(filteredLights, args.filter), }; // Add efficiency tips if (lightsWithContext.length > 15) { result.suggestions.push('đź’ˇ Tip: Use find_lights with specific criteria for faster results'); } return result; } catch (error) { throw new Error(`Failed to list lights: ${error}`); } } function generateLightSuggestions(lights: any[], filter?: string): string[] { const suggestions: string[] = []; const unreachable = lights.filter(l => !l.state.reachable); if (unreachable.length > 0) { suggestions.push(`${unreachable.length} lights are unreachable and may need attention`); } const onLights = lights.filter(l => l.state.on); if (!filter && onLights.length > lights.length * 0.8) { suggestions.push('Most lights are on - consider using scenes for energy efficiency'); } if (filter && lights.length === 0) { suggestions.push(`No lights match filter "${filter}". Try "on", "off", "kitchen", or a room name`); } return suggestions; } export async function handleGetLight(client: HueClient, args: any): Promise<any> { try { const light = await client.getLight(args.lightId); if (!light) { throw new Error(`Light ${args.lightId} not found`); } // Find which room this light belongs to const rooms = await client.getRooms(); const parentRoom = rooms.find(room => room.lights.includes(args.lightId)); const result = { id: light.id, name: light.name, type: light.type, modelId: light.modelid, manufacturerName: light.manufacturername, productName: light.productname, uniqueId: light.uniqueid, softwareVersion: light.swversion, room: parentRoom ? { id: parentRoom.id, name: parentRoom.name } : null, state: { on: light.state.on, brightness: Math.round((light.state.bri / 254) * 100), reachable: light.state.reachable, colorMode: light.state.colormode, ...(light.state.hue !== undefined && { hue: Math.round((light.state.hue / 65535) * 360), saturation: Math.round((light.state.sat / 254) * 100), }), ...(light.state.ct !== undefined && { colorTemp: light.state.ct }), ...(light.state.xy && { xy: light.state.xy }), alert: light.state.alert, effect: light.state.effect, }, capabilities: { ...light.capabilities, supportsColor: light.capabilities?.control?.colorgamut !== undefined, supportsColorTemp: light.capabilities?.control?.ct !== undefined, supportsBrightness: light.capabilities?.control?.mindimlevel !== undefined, maxBrightness: light.capabilities?.control?.maxlumen, }, config: light.config, quickActions: generateQuickActions(light), recommendations: generateLightRecommendations(light), }; return result; } catch (error) { throw new Error(`Failed to get light: ${error}`); } } function generateQuickActions(light: any): any[] { const actions: any[] = []; if (light.state.on) { actions.push({ action: 'Turn Off', naturalLanguage: 'turn off', state: { on: false } }); if (light.state.bri > 50) { actions.push({ action: 'Dim to 25%', naturalLanguage: 'dim to 25%', state: { brightness: 25 } }); } } else { actions.push({ action: 'Turn On', naturalLanguage: 'turn on', state: { on: true } }); } // Color suggestions if supported if (light.capabilities?.control?.colorgamut) { actions.push({ action: 'Warm White', naturalLanguage: 'warm white', state: { on: true, colorTemp: 366 } }); actions.push({ action: 'Cool White', naturalLanguage: 'cool white', state: { on: true, colorTemp: 153 } }); actions.push({ action: 'Soft Blue', naturalLanguage: 'soft blue mood lighting', state: { on: true, hue: 240, saturation: 30, brightness: 50 } }); } return actions; } function generateLightRecommendations(light: any): string[] { const recommendations: string[] = []; if (!light.state.reachable) { recommendations.push('Light is unreachable - check power and network connection'); } if (light.state.on && light.state.bri < 50) { recommendations.push('Light is quite dim - consider increasing brightness for better visibility'); } if (light.capabilities?.control?.colorgamut && light.state.colormode === 'ct') { recommendations.push('This light supports color - try setting a mood with colored lighting'); } if (light.swversion && parseInt(light.swversion.split('.')[0]) < 2) { recommendations.push('Light firmware may be outdated - check for updates in the Hue app'); } return recommendations; } export async function handleSetLightState(client: HueClient, args: any): Promise<any> { try { let state: LightState; if (args.naturalLanguage) { state = HueClient.parseNaturalLanguageState(args.naturalLanguage); } else { state = args.state; } const success = await client.setLightState(args.lightId, state); if (!success) { throw new Error(`Failed to update light ${args.lightId}`); } // Get updated light state const light = await client.getLight(args.lightId); return { success: true, lightId: args.lightId, appliedState: state, currentState: light ? { on: light.state.on, brightness: light.state.bri, hue: light.state.hue, saturation: light.state.sat, colorTemp: light.state.ct, reachable: light.state.reachable, } : null, }; } catch (error) { throw new Error(`Failed to set light state: ${error}`); } }

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/rmrfslashbin/hue-mcp'

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