Skip to main content
Glama

Philips Hue MCP Server

by ThomasRohde
hue_server.py35.7 kB
""" Philips Hue Controller MCP Server This server provides a Model Context Protocol (MCP) interface to control Philips Hue lights. It exposes resources for retrieving light information and tools for controlling lights. Requirements: - phue: Python library for Philips Hue API - mcp: Model Context Protocol Python SDK Setup: 1. Install dependencies: pip install phue mcp 2. Update the bridge_ip in the config section or use bridge discovery 3. Run the server: python hue_server.py 4. Press the link button on your Hue bridge when prompted during first run """ from mcp.server.fastmcp import FastMCP, Context from phue import Bridge import json import os import logging from typing import Dict, List, Optional, Tuple from contextlib import asynccontextmanager from collections.abc import AsyncIterator from dataclasses import dataclass # --- Configuration --- # You can customize these values or load from a config file # Bridge IP - can be set to None for auto-discovery BRIDGE_IP = None # Example: "192.168.1.100" # Path to store bridge connection info CONFIG_DIR = os.path.expanduser("~/.hue-mcp") CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json") # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger("hue-mcp") # --- Server Context Setup --- @dataclass class HueContext: """Context object holding the Hue bridge connection.""" bridge: Bridge light_info: Dict # Cache of light information @asynccontextmanager async def hue_lifespan(server: FastMCP) -> AsyncIterator[HueContext]: """ Manage connection to Hue Bridge. This function handles: 1. Discovery or connection to the bridge 2. Storing/loading connection info 3. Building a cache of light information """ # Ensure config directory exists os.makedirs(CONFIG_DIR, exist_ok=True) # Load saved config if it exists bridge_ip = BRIDGE_IP bridge_username = None if os.path.exists(CONFIG_FILE): try: with open(CONFIG_FILE, 'r') as f: config = json.load(f) bridge_ip = config.get('bridge_ip', bridge_ip) bridge_username = config.get('username') logger.info(f"Loaded configuration from {CONFIG_FILE}") except Exception as e: logger.error(f"Error loading config: {e}") # Initialize Bridge try: # If no IP specified, attempt discovery if not bridge_ip: logger.info("No bridge IP specified, attempting discovery...") bridge = Bridge() # This will attempt discovery bridge_ip = bridge.ip logger.info(f"Discovered bridge at {bridge_ip}") else: logger.info(f"Connecting to bridge at {bridge_ip}") bridge = Bridge(bridge_ip, username=bridge_username) # Connect to the bridge (this may prompt user to press the link button) bridge.connect() # Save the configuration with open(CONFIG_FILE, 'w') as f: json.dump({ 'bridge_ip': bridge.ip, 'username': bridge.username }, f) logger.info(f"Saved configuration to {CONFIG_FILE}") # Build a cache of light information for faster access light_info = bridge.get_light() # Initialize and yield the context yield HueContext(bridge=bridge, light_info=light_info) except Exception as e: logger.error(f"Error connecting to Hue bridge: {e}") # Re-raise to inform the server of the failure raise finally: # No explicit cleanup needed for bridge pass # Create MCP server mcp = FastMCP( "Philips Hue Controller", lifespan=hue_lifespan, dependencies=["phue"] ) # --- Utility Functions --- def get_bridge_ctx(ctx: Context) -> Tuple[Bridge, Dict]: """Get the Hue bridge and light info from context.""" hue_ctx = ctx.request_context.lifespan_context return hue_ctx.bridge, hue_ctx.light_info def rgb_to_xy(r: int, g: int, b: int) -> List[float]: """ Convert RGB values to XY color space used by Hue. Args: r: Red value (0-255) g: Green value (0-255) b: Blue value (0-255) Returns: List containing [x, y] coordinates in the CIE color space """ # Normalize RGB values r, g, b = r/255.0, g/255.0, b/255.0 # Apply gamma correction r = pow(r, 2.2) if r > 0.04045 else r/12.92 g = pow(g, 2.2) if g > 0.04045 else g/12.92 b = pow(b, 2.2) if b > 0.04045 else b/12.92 # Convert to XYZ using the Wide RGB D65 conversion matrix X = r * 0.649926 + g * 0.103455 + b * 0.197109 Y = r * 0.234327 + g * 0.743075 + b * 0.022598 Z = r * 0.000000 + g * 0.053077 + b * 1.035763 # Calculate xy values from XYZ sum_XYZ = X + Y + Z if sum_XYZ == 0: return [0, 0] x = X / sum_XYZ y = Y / sum_XYZ return [x, y] def validate_light_id(light_id: int, light_info: Dict) -> bool: """Validate that a light ID exists.""" return str(light_id) in light_info def validate_group_id(group_id: int, bridge: Bridge) -> bool: """Validate that a group ID exists.""" groups = bridge.get_group() return str(group_id) in groups def format_light_info(light_info: Dict) -> Dict: """Format light information for display.""" result = {} for light_id, light in light_info.items(): # Extract the most useful information result[light_id] = { "name": light["name"], "on": light["state"]["on"], "reachable": light["state"].get("reachable", True), "brightness": light["state"].get("bri"), "color_mode": light["state"].get("colormode"), "type": light["type"], "model": light.get("modelid"), "manufacturer": light.get("manufacturername"), } return result # --- Convert Resources to Tools --- @mcp.tool() def get_all_lights(ctx: Context) -> str: """ Get information about all lights connected to the Hue bridge. Returns: JSON string containing information about all lights """ bridge, light_info = get_bridge_ctx(ctx) # Format the light information for better readability formatted_info = format_light_info(light_info) return json.dumps(formatted_info, indent=2) @mcp.tool() def get_light(light_id: int, ctx: Context) -> str: """ Get detailed information about a specific light. Args: light_id: The ID of the light Returns: JSON string containing detailed information about the light """ bridge, light_info = get_bridge_ctx(ctx) try: # Convert light_id to string for dict lookup light_id_str = str(light_id) # Check if the light exists if light_id_str not in light_info: return f"Error: Light with ID {light_id} not found." return json.dumps(light_info[light_id_str], indent=2) except Exception as e: logger.error(f"Error getting light {light_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def get_all_groups(ctx: Context) -> str: """ Get information about all light groups. Returns: JSON string containing information about all groups """ bridge, _ = get_bridge_ctx(ctx) try: groups = bridge.get_group() # Format the groups for better readability formatted_groups = {} for group_id, group in groups.items(): formatted_groups[group_id] = { "name": group["name"], "type": group["type"], "lights": group["lights"], "on": group["state"]["all_on"], "any_on": group["state"]["any_on"] } return json.dumps(formatted_groups, indent=2) except Exception as e: logger.error(f"Error getting groups: {e}") return f"Error: {str(e)}" @mcp.tool() def get_group(group_id: int, ctx: Context) -> str: """ Get information about a specific light group. Args: group_id: The ID of the group Returns: JSON string containing information about the group """ bridge, _ = get_bridge_ctx(ctx) try: groups = bridge.get_group() # Convert group_id to string for dict lookup group_id_str = str(group_id) # Check if the group exists if group_id_str not in groups: return f"Error: Group with ID {group_id} not found." return json.dumps(groups[group_id_str], indent=2) except Exception as e: logger.error(f"Error getting group {group_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def get_all_scenes(ctx: Context) -> str: """ Get information about all scenes. Returns: JSON string containing information about all scenes """ bridge, _ = get_bridge_ctx(ctx) try: scenes = bridge.get_scene() # Format the scenes for better readability formatted_scenes = {} for scene_id, scene in scenes.items(): formatted_scenes[scene_id] = { "name": scene.get("name", "Unknown"), "type": scene.get("type", "Unknown"), "group": scene.get("group"), "lights": scene.get("lights", []), "owner": scene.get("owner") } return json.dumps(formatted_scenes, indent=2) except Exception as e: logger.error(f"Error getting scenes: {e}") return f"Error: {str(e)}" # --- Tools --- @mcp.tool() def turn_on_light(light_id: int, ctx: Context) -> str: """ Turn on a specific light by ID. Args: light_id: The ID of the light to turn on Returns: Confirmation message """ bridge, light_info = get_bridge_ctx(ctx) try: # Validate light ID if not validate_light_id(light_id, light_info): return f"Error: Light with ID {light_id} not found." bridge.set_light(light_id, 'on', True) return f"Light {light_id} ({light_info[str(light_id)]['name']}) turned on." except Exception as e: logger.error(f"Error turning on light {light_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def turn_off_light(light_id: int, ctx: Context) -> str: """ Turn off a specific light by ID. Args: light_id: The ID of the light to turn off Returns: Confirmation message """ bridge, light_info = get_bridge_ctx(ctx) try: # Validate light ID if not validate_light_id(light_id, light_info): return f"Error: Light with ID {light_id} not found." bridge.set_light(light_id, 'on', False) return f"Light {light_id} ({light_info[str(light_id)]['name']}) turned off." except Exception as e: logger.error(f"Error turning off light {light_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def set_brightness(light_id: int, brightness: int, ctx: Context) -> str: """ Set the brightness of a light. Args: light_id: The ID of the light brightness: Brightness level (0-254) Returns: Confirmation message """ if not 0 <= brightness <= 254: return "Error: Brightness must be between 0 and 254." bridge, light_info = get_bridge_ctx(ctx) try: # Validate light ID if not validate_light_id(light_id, light_info): return f"Error: Light with ID {light_id} not found." # Turn on the light if it's off if not light_info[str(light_id)]['state']['on']: bridge.set_light(light_id, 'on', True) bridge.set_light(light_id, 'bri', brightness) # Calculate brightness percentage for user feedback percentage = round((brightness / 254) * 100) return f"Light {light_id} ({light_info[str(light_id)]['name']}) brightness set to {brightness} ({percentage}%)." except Exception as e: logger.error(f"Error setting brightness for light {light_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def set_color_rgb(light_id: int, red: int, green: int, blue: int, ctx: Context) -> str: """ Set light color using RGB values. Args: light_id: The ID of the light red: Red value (0-255) green: Green value (0-255) blue: Blue value (0-255) Returns: Confirmation message """ if not all(0 <= c <= 255 for c in (red, green, blue)): return "Error: RGB values must be between 0 and 255." bridge, light_info = get_bridge_ctx(ctx) try: # Validate light ID if not validate_light_id(light_id, light_info): return f"Error: Light with ID {light_id} not found." # Check if light supports color if 'xy' not in light_info[str(light_id)]['state']: return f"Error: Light {light_id} ({light_info[str(light_id)]['name']}) does not support color." # Turn on the light if it's off if not light_info[str(light_id)]['state']['on']: bridge.set_light(light_id, 'on', True) xy = rgb_to_xy(red, green, blue) bridge.set_light(light_id, 'xy', xy) return f"Light {light_id} ({light_info[str(light_id)]['name']}) color set to RGB({red}, {green}, {blue})." except Exception as e: logger.error(f"Error setting RGB color for light {light_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def turn_on_group(group_id: int, ctx: Context) -> str: """ Turn on all lights in a specific group. Args: group_id: The ID of the group Returns: Confirmation message """ bridge, _ = get_bridge_ctx(ctx) try: # Validate group ID if not validate_group_id(group_id, bridge): return f"Error: Group with ID {group_id} not found." # Get group info for name group_info = bridge.get_group(group_id) group_name = group_info.get('name', f"Group {group_id}") bridge.set_group(group_id, 'on', True) return f"Group {group_id} ({group_name}) turned on." except Exception as e: logger.error(f"Error turning on group {group_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def turn_off_group(group_id: int, ctx: Context) -> str: """ Turn off all lights in a specific group. Args: group_id: The ID of the group Returns: Confirmation message """ bridge, _ = get_bridge_ctx(ctx) try: # Validate group ID if not validate_group_id(group_id, bridge): return f"Error: Group with ID {group_id} not found." # Get group info for name group_info = bridge.get_group(group_id) group_name = group_info.get('name', f"Group {group_id}") bridge.set_group(group_id, 'on', False) return f"Group {group_id} ({group_name}) turned off." except Exception as e: logger.error(f"Error turning off group {group_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def set_group_brightness(group_id: int, brightness: int, ctx: Context) -> str: """ Set the brightness of all lights in a group. Args: group_id: The ID of the group brightness: Brightness level (0-254) Returns: Confirmation message """ if not 0 <= brightness <= 254: return "Error: Brightness must be between 0 and 254." bridge, _ = get_bridge_ctx(ctx) try: # Validate group ID if not validate_group_id(group_id, bridge): return f"Error: Group with ID {group_id} not found." # Get group info for name group_info = bridge.get_group(group_id) group_name = group_info.get('name', f"Group {group_id}") # Turn on the group if it's off if not group_info['state']['any_on']: bridge.set_group(group_id, 'on', True) bridge.set_group(group_id, 'bri', brightness) # Calculate brightness percentage for user feedback percentage = round((brightness / 254) * 100) return f"Group {group_id} ({group_name}) brightness set to {brightness} ({percentage}%)." except Exception as e: logger.error(f"Error setting brightness for group {group_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def set_group_color_rgb(group_id: int, red: int, green: int, blue: int, ctx: Context) -> str: """ Set color for all lights in a group using RGB values. Args: group_id: The ID of the group red: Red value (0-255) green: Green value (0-255) blue: Blue value (0-255) Returns: Confirmation message """ if not all(0 <= c <= 255 for c in (red, green, blue)): return "Error: RGB values must be between 0 and 255." bridge, _ = get_bridge_ctx(ctx) try: # Validate group ID if not validate_group_id(group_id, bridge): return f"Error: Group with ID {group_id} not found." # Get group info for name group_info = bridge.get_group(group_id) group_name = group_info.get('name', f"Group {group_id}") # Turn on the group if it's off if not group_info['state']['any_on']: bridge.set_group(group_id, 'on', True) xy = rgb_to_xy(red, green, blue) bridge.set_group(group_id, 'xy', xy) return f"Group {group_id} ({group_name}) color set to RGB({red}, {green}, {blue})." except Exception as e: logger.error(f"Error setting color for group {group_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def set_scene(group_id: int, scene_id: str, ctx: Context) -> str: """ Apply a scene to a group. Args: group_id: The ID of the group scene_id: The ID of the scene Returns: Confirmation message """ bridge, _ = get_bridge_ctx(ctx) try: # Validate group ID if not validate_group_id(group_id, bridge): return f"Error: Group with ID {group_id} not found." # Verify the scene exists scenes = bridge.get_scene() if scene_id not in scenes: return f"Error: Scene with ID {scene_id} not found." # Get names for better feedback group_name = bridge.get_group(group_id).get('name', f"Group {group_id}") scene_name = scenes[scene_id].get('name', f"Scene {scene_id}") bridge.set_group(group_id, 'scene', scene_id) return f"Scene '{scene_name}' applied to group '{group_name}'." except Exception as e: logger.error(f"Error applying scene {scene_id} to group {group_id}: {e}") return f"Error: {str(e)}" # --- Helper Tools --- @mcp.tool() def find_light_by_name(name: str, ctx: Context) -> str: """ Find lights by searching their names. Args: name: Partial or full name to search for Returns: JSON string containing matching lights """ _, light_info = get_bridge_ctx(ctx) try: # Search for lights with matching names (case-insensitive) name_lower = name.lower() matches = {} for light_id, light in light_info.items(): if name_lower in light['name'].lower(): matches[light_id] = { "id": light_id, "name": light['name'], "type": light['type'], "on": light['state']['on'] } if not matches: return f"No lights found with name containing '{name}'." return json.dumps(matches, indent=2) except Exception as e: logger.error(f"Error finding lights by name '{name}': {e}") return f"Error: {str(e)}" @mcp.tool() def create_group( name: str, light_ids: List[int], ctx: Context ) -> str: """ Create a new group of lights. Args: name: Name for the new group light_ids: List of light IDs to include in the group Returns: Confirmation message with group ID """ bridge, light_info = get_bridge_ctx(ctx) try: # Validate all light IDs invalid_lights = [lid for lid in light_ids if not validate_light_id(lid, light_info)] if invalid_lights: return f"Error: Invalid light IDs: {invalid_lights}" # Convert light IDs to strings (Hue API requirement) light_id_strings = [str(lid) for lid in light_ids] # Create the group result = bridge.create_group(name, light_id_strings) # Extract the group ID from the result if 'success' in result[0]: # Extract the ID from the success response # Format is usually: {"success":{"id":"/groups/1"}} success_path = list(result[0]['success'].values())[0] group_id = success_path.split('/')[-1] return f"Group '{name}' created with ID {group_id}, containing {len(light_ids)} lights." else: return f"Error creating group: {result}" except Exception as e: logger.error(f"Error creating group '{name}': {e}") return f"Error: {str(e)}" @mcp.tool() def quick_scene( name: str, ctx: Context, rgb: Optional[List[int]] = None, temperature: Optional[int] = None, brightness: Optional[int] = None, group_id: int = 0 # Default to group 0 (usually "All lights") ) -> str: """ Quickly set up a lighting scene for a group. Args: name: Name for the scene rgb: Optional RGB values [r, g, b] temperature: Optional color temperature (2000-6500K) brightness: Optional brightness (0-254) group_id: Group ID to apply settings to (default: 0, usually "All lights") Returns: Confirmation message """ bridge, light_info = get_bridge_ctx(ctx) try: # Validate group ID if not validate_group_id(group_id, bridge): return f"Error: Group with ID {group_id} not found." # Get group info for name group_info = bridge.get_group(group_id) group_name = group_info.get('name', f"Group {group_id}") # Turn on the group bridge.set_group(group_id, 'on', True) # Apply settings if brightness is not None: if not 0 <= brightness <= 254: return "Error: Brightness must be between 0 and 254." bridge.set_group(group_id, 'bri', brightness) if rgb is not None: if not all(0 <= c <= 255 for c in rgb) or len(rgb) != 3: return "Error: RGB values must be three values between 0 and 255." xy = rgb_to_xy(rgb[0], rgb[1], rgb[2]) bridge.set_group(group_id, 'xy', xy) if temperature is not None: if not 2000 <= temperature <= 6500: return "Error: Temperature must be between 2000K and 6500K." # Convert temperature in K to mired mired = int(1000000 / temperature) bridge.set_group(group_id, 'ct', mired) # Return a summary of what was applied changes = [] if brightness is not None: changes.append(f"brightness {brightness} ({round((brightness / 254) * 100)}%)") if rgb is not None: changes.append(f"color RGB({rgb[0]}, {rgb[1]}, {rgb[2]})") if temperature is not None: changes.append(f"temperature {temperature}K") return f"Scene '{name}' applied to group '{group_name}' with {', '.join(changes)}." except Exception as e: logger.error(f"Error applying quick scene '{name}': {e}") return f"Error: {str(e)}" @mcp.tool() def refresh_lights(ctx: Context) -> str: """ Refresh the light information cache. This is useful if lights have been added or removed, or if their state has changed outside this application. Returns: Information about the refreshed lights """ bridge, _ = get_bridge_ctx(ctx) try: # Update the bridge's internal state bridge.get_api() # Update our cache light_info = bridge.get_light() ctx.request_context.lifespan_context.light_info = light_info return f"Refreshed information for {len(light_info)} lights." except Exception as e: logger.error(f"Error refreshing lights: {e}") return f"Error: {str(e)}" @mcp.tool() def set_color_preset( light_id: int, preset: str, ctx: Context ) -> str: """ Apply a color preset to a light. Args: light_id: The ID of the light preset: Color preset name (warm, cool, daylight, concentration, relax, reading, energize, red, green, blue, purple, orange) Returns: Confirmation message """ # Define color presets with RGB values presets = { # White temperature presets "warm": {"ct": 2500}, # Warm white (2500K) "cool": {"ct": 4500}, # Cool white (4500K) "daylight": {"ct": 6500}, # Daylight (6500K) # Activity presets (Philips recommended settings) "concentration": {"ct": 4600, "bri": 254}, # Bright cool light "relax": {"ct": 2700, "bri": 144}, # Warm dimmed light "reading": {"ct": 3200, "bri": 219}, # Moderate neutral light "energize": {"ct": 6000, "bri": 254}, # Bright blue light # Color presets "red": {"xy": rgb_to_xy(255, 0, 0)}, "green": {"xy": rgb_to_xy(0, 255, 0)}, "blue": {"xy": rgb_to_xy(0, 0, 255)}, "purple": {"xy": rgb_to_xy(128, 0, 128)}, "orange": {"xy": rgb_to_xy(255, 165, 0)}, } if preset not in presets: return f"Error: Unknown preset. Available presets: {', '.join(presets.keys())}" bridge, light_info = get_bridge_ctx(ctx) try: # Validate light ID if not validate_light_id(light_id, light_info): return f"Error: Light with ID {light_id} not found." # Check capability for color temperature if "ct" in presets[preset] and 'ct' not in light_info[str(light_id)]['state']: return f"Error: Light {light_id} does not support color temperature." # Check capability for xy color if "xy" in presets[preset] and 'xy' not in light_info[str(light_id)]['state']: return f"Error: Light {light_id} does not support color." # Turn on the light if it's off if not light_info[str(light_id)]['state']['on']: bridge.set_light(light_id, 'on', True) # Apply preset settings for key, value in presets[preset].items(): bridge.set_light(light_id, key, value) return f"Applied '{preset}' preset to light {light_id} ({light_info[str(light_id)]['name']})." except Exception as e: logger.error(f"Error applying preset '{preset}' to light {light_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def set_group_color_preset( group_id: int, preset: str, ctx: Context ) -> str: """ Apply a color preset to a group. Args: group_id: The ID of the group preset: Color preset name (warm, cool, daylight, concentration, relax, reading, energize, red, green, blue, purple, orange) Returns: Confirmation message """ # Define color presets with RGB values (same as in set_color_preset) presets = { # White temperature presets "warm": {"ct": 2500}, # Warm white (2500K) "cool": {"ct": 4500}, # Cool white (4500K) "daylight": {"ct": 6500}, # Daylight (6500K) # Activity presets (Philips recommended settings) "concentration": {"ct": 4600, "bri": 254}, # Bright cool light "relax": {"ct": 2700, "bri": 144}, # Warm dimmed light "reading": {"ct": 3200, "bri": 219}, # Moderate neutral light "energize": {"ct": 6000, "bri": 254}, # Bright blue light # Color presets "red": {"xy": rgb_to_xy(255, 0, 0)}, "green": {"xy": rgb_to_xy(0, 255, 0)}, "blue": {"xy": rgb_to_xy(0, 0, 255)}, "purple": {"xy": rgb_to_xy(128, 0, 128)}, "orange": {"xy": rgb_to_xy(255, 165, 0)}, } if preset not in presets: return f"Error: Unknown preset. Available presets: {', '.join(presets.keys())}" bridge, _ = get_bridge_ctx(ctx) try: # Validate group ID if not validate_group_id(group_id, bridge): return f"Error: Group with ID {group_id} not found." # Get group info for name group_info = bridge.get_group(group_id) group_name = group_info.get('name', f"Group {group_id}") # Turn on the group if it's off if not group_info['state']['any_on']: bridge.set_group(group_id, 'on', True) # Apply preset settings for key, value in presets[preset].items(): bridge.set_group(group_id, key, value) return f"Applied '{preset}' preset to group '{group_name}'." except Exception as e: logger.error(f"Error applying preset '{preset}' to group {group_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def alert_light(light_id: int, ctx: Context) -> str: """ Make a light flash briefly to identify it. Args: light_id: The ID of the light to alert Returns: Confirmation message """ bridge, light_info = get_bridge_ctx(ctx) try: # Validate light ID if not validate_light_id(light_id, light_info): return f"Error: Light with ID {light_id} not found." # Use the alert feature of Hue lights bridge.set_light(light_id, 'alert', 'select') return f"Light {light_id} ({light_info[str(light_id)]['name']}) alerted with a brief flash." except Exception as e: logger.error(f"Error alerting light {light_id}: {e}") return f"Error: {str(e)}" @mcp.tool() def set_light_effect(light_id: int, effect: str, ctx: Context) -> str: """ Set a dynamic effect on a light. Args: light_id: The ID of the light effect: Effect type ('none' or 'colorloop') Returns: Confirmation message """ # Validate effect type valid_effects = ['none', 'colorloop'] if effect not in valid_effects: return f"Error: Effect must be one of: {', '.join(valid_effects)}" bridge, light_info = get_bridge_ctx(ctx) try: # Validate light ID if not validate_light_id(light_id, light_info): return f"Error: Light with ID {light_id} not found." # Check if light supports color (needed for effects) if 'xy' not in light_info[str(light_id)]['state']: return f"Error: Light {light_id} ({light_info[str(light_id)]['name']}) does not support color effects." # Turn on the light if it's off if not light_info[str(light_id)]['state']['on']: bridge.set_light(light_id, 'on', True) bridge.set_light(light_id, 'effect', effect) effect_name = "color loop" if effect == "colorloop" else "no effect" return f"Set {effect_name} on light {light_id} ({light_info[str(light_id)]['name']})." except Exception as e: logger.error(f"Error setting effect {effect} on light {light_id}: {e}") return f"Error: {str(e)}" # --- Prompts --- @mcp.prompt() def control_lights() -> str: """ A prompt for controlling lights with natural language. """ return """ You are connected to a Philips Hue lighting system. I want to control my lights using natural language. Please help me interpret my requests and use the appropriate tools to control my lighting. First, if needed, retrieve information about my lights using the resources: hue://lights and hue://groups. Then, use the appropriate tools to control the lights based on my request. For example: - Turn on or off specific lights or groups - Change brightness or color - Apply presets for different activities - Set scenes or effects Please confirm each action you take and provide feedback on the results. """ @mcp.prompt() def create_mood() -> str: """ A prompt for setting up mood lighting. """ return """ You are connected to my Philips Hue lighting system. I want to create mood lighting for a specific activity or atmosphere. Please help me set up the perfect lighting environment. First, gather information about my available lights and groups. Then, suggest and implement a lighting setup based on my mood or activity request. Consider: - Appropriate brightness levels for the activity - Color temperature or colors that match the mood - Using preset scenes or creating custom settings - Grouping lights appropriately After implementing, summarize what you've done and ask if I'd like to make adjustments. """ @mcp.prompt() def light_schedule() -> str: """ A prompt for explaining how to set up lighting schedules. """ return """ I'd like to understand how to set up scheduled lighting with my Philips Hue system. Please explain the options available for scheduling automatic lighting changes, including: - Whether scheduling is handled through the Hue app rather than this interface - The types of schedules I can create (time-based, sunrise/sunset, etc.) - How to create routines or scenes that can be scheduled - Any limitations I should be aware of After explaining the scheduling capabilities, suggest some useful lighting schedules for typical home use. """ # --- Main Function --- if __name__ == "__main__": import uvicorn import argparse parser = argparse.ArgumentParser(description="Philips Hue MCP Server") parser.add_argument("--port", type=int, default=8080, help="Port to run the server on") parser.add_argument("--host", type=str, default="127.0.0.1", help="Host to bind the server to") parser.add_argument("--log-level", type=str, default="info", choices=["debug", "info", "warning", "error", "critical"], help="Logging level") args = parser.parse_args() # Set up logging level log_level = getattr(logging, args.log_level.upper()) logging.getLogger("hue-mcp").setLevel(log_level) print(f"Starting Philips Hue MCP Server on {args.host}:{args.port}") print("Press Ctrl+C to stop the server") # Run the server using mcp.run() or manually with Uvicorn # mcp.run(host=args.host, port=args.port) # Use this for direct execution # Or use Uvicorn for more control uvicorn.run( mcp.sse_app(), host=args.host, port=args.port, log_level=args.log_level )

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

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