"""
MCP tool definitions for Philips Hue control.
This module defines all the MCP tools that will be exposed by the server.
"""
from typing import Any, Dict, List
from .hue_client import (
HueClient,
BRIGHTNESS_MIN_HUE_API,
BRIGHTNESS_MAX_HUE_API,
COLOR_TEMP_MIN_MIREDS,
COLOR_TEMP_MAX_MIREDS,
CIE_XY_MIN,
CIE_XY_MAX,
)
# Validation helper functions
def _validate_light_id(light_id: Any) -> None:
"""
Validate that a light ID is provided.
Args:
light_id: The light ID to validate
Raises:
ValueError: If light_id is None or empty
"""
if not light_id:
raise ValueError("light_id is required")
def _validate_brightness(brightness: Any) -> None:
"""
Validate brightness value is within acceptable range.
Args:
brightness: The brightness value to validate
Raises:
ValueError: If brightness is not a number or out of range (0-254)
"""
if not isinstance(brightness, (int, float)):
raise ValueError(
f"brightness must be a number, got {type(brightness).__name__}"
)
if brightness < BRIGHTNESS_MIN_HUE_API or brightness > BRIGHTNESS_MAX_HUE_API:
raise ValueError(
f"brightness must be between {BRIGHTNESS_MIN_HUE_API} and "
f"{BRIGHTNESS_MAX_HUE_API}"
)
def _validate_color_temperature(color_temp: Any) -> None:
"""
Validate color temperature value is within acceptable range.
Args:
color_temp: The color temperature value to validate
Raises:
ValueError: If color_temp is not a number or out of range (153-500 mireds)
"""
if not isinstance(color_temp, (int, float)):
raise ValueError(
f"color_temp must be a number, got {type(color_temp).__name__}"
)
if color_temp < COLOR_TEMP_MIN_MIREDS or color_temp > COLOR_TEMP_MAX_MIREDS:
raise ValueError(
f"color_temp must be between {COLOR_TEMP_MIN_MIREDS} and "
f"{COLOR_TEMP_MAX_MIREDS} mireds"
)
def _validate_xy_coordinates(xy: Any) -> None:
"""
Validate CIE xy color coordinates.
Args:
xy: The xy coordinates to validate
Raises:
ValueError: If xy is not a valid list of two floats between 0 and 1
"""
if not isinstance(xy, list):
raise ValueError(f"xy must be a list, got {type(xy).__name__}")
if len(xy) != 2:
raise ValueError(f"xy must be a list of two values [x, y], got {len(xy)} values")
for i, value in enumerate(xy):
coordinate_name = "x" if i == 0 else "y"
if not isinstance(value, (int, float)):
raise ValueError(
f"xy[{i}] ({coordinate_name}) must be a number, got {type(value).__name__}"
)
if value < CIE_XY_MIN or value > CIE_XY_MAX:
raise ValueError(
f"xy[{i}] ({coordinate_name}) must be between {CIE_XY_MIN} and "
f"{CIE_XY_MAX}"
)
async def list_lights_tool(client: HueClient, arguments: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
List all lights connected to the Hue Bridge.
Returns:
List of lights with their current state
"""
lights = await client.get_lights()
return [
{
"id": light_id,
"name": light_data["name"],
"on": light_data["on"],
"brightness": light_data["brightness"],
"color_temp": light_data["color_temp"],
"reachable": light_data["reachable"],
}
for light_id, light_data in lights.items()
]
async def get_light_state_tool(client: HueClient, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""
Get the current state of a specific light.
Args:
arguments: Must contain 'light_id' key
Returns:
Detailed state of the light
"""
light_id = arguments.get("light_id")
_validate_light_id(light_id)
light = await client.get_light(light_id)
if not light:
raise ValueError(f"Light with ID '{light_id}' not found")
return light
async def turn_light_on_tool(client: HueClient, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""
Turn a light on, optionally setting brightness.
Args:
arguments: Must contain 'light_id', optionally 'brightness' (0-254)
Returns:
Success message
"""
light_id = arguments.get("light_id")
brightness = arguments.get("brightness")
_validate_light_id(light_id)
if brightness is not None:
_validate_brightness(brightness)
await client.set_light_state(light_id, on=True, brightness=brightness)
brightness_message = f" with brightness {brightness}" if brightness is not None else ""
return {
"success": True,
"message": f"Light {light_id} turned on{brightness_message}",
}
async def turn_light_off_tool(client: HueClient, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""
Turn a light off.
Args:
arguments: Must contain 'light_id'
Returns:
Success message
"""
light_id = arguments.get("light_id")
_validate_light_id(light_id)
await client.set_light_state(light_id, on=False)
return {"success": True, "message": f"Light {light_id} turned off"}
async def set_brightness_tool(client: HueClient, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""
Set the brightness of a light.
Args:
arguments: Must contain 'light_id' and 'brightness' (0-254)
Returns:
Success message
"""
light_id = arguments.get("light_id")
brightness = arguments.get("brightness")
_validate_light_id(light_id)
if brightness is None:
raise ValueError("brightness is required")
_validate_brightness(brightness)
await client.set_light_state(light_id, brightness=int(brightness))
return {"success": True, "message": f"Light {light_id} brightness set to {brightness}"}
async def set_color_temp_tool(client: HueClient, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""
Set the color temperature of a light.
Args:
arguments: Must contain 'light_id' and 'color_temp' (153-500 mireds)
Returns:
Success message
"""
light_id = arguments.get("light_id")
color_temp = arguments.get("color_temp")
_validate_light_id(light_id)
if color_temp is None:
raise ValueError("color_temp is required")
_validate_color_temperature(color_temp)
await client.set_light_state(light_id, color_temp=int(color_temp))
return {"success": True, "message": f"Light {light_id} color temperature set to {color_temp} mireds"}
async def set_color_tool(client: HueClient, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""
Set the color of a light using CIE xy coordinates.
Args:
arguments: Must contain 'light_id' and 'xy' (list of two floats [x, y])
Returns:
Success message
"""
light_id = arguments.get("light_id")
xy = arguments.get("xy")
_validate_light_id(light_id)
if not xy:
raise ValueError("xy is required")
_validate_xy_coordinates(xy)
await client.set_light_state(light_id, xy=xy)
return {"success": True, "message": f"Light {light_id} color set to xy={xy}"}
async def list_groups_tool(client: HueClient, arguments: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
List all groups/rooms.
Returns:
List of groups with their properties
"""
groups = await client.get_groups()
return [
{
"id": group_id,
"name": group_data["name"],
"type": group_data["type"],
"lights": group_data["lights"],
}
for group_id, group_data in groups.items()
]
async def control_group_tool(client: HueClient, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""
Control all lights in a group.
Args:
arguments: Must contain 'group_id', optionally 'on', 'brightness', 'color_temp', 'xy'
Returns:
Success message
"""
group_id = arguments.get("group_id")
on = arguments.get("on")
brightness = arguments.get("brightness")
color_temp = arguments.get("color_temp")
xy = arguments.get("xy")
if not group_id:
raise ValueError("group_id is required")
# Validate optional parameters if provided
if brightness is not None:
_validate_brightness(brightness)
if color_temp is not None:
_validate_color_temperature(color_temp)
if xy is not None:
_validate_xy_coordinates(xy)
await client.set_group_state(
group_id=group_id,
on=on,
brightness=int(brightness) if brightness is not None else None,
color_temp=int(color_temp) if color_temp is not None else None,
xy=xy,
)
return {"success": True, "message": f"Group {group_id} state updated"}
async def list_scenes_tool(client: HueClient, arguments: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
List all available scenes.
Returns:
List of scenes with their properties
"""
scenes = await client.get_scenes()
return [
{
"id": scene_id,
"name": scene_data["name"],
"group": scene_data["group"],
}
for scene_id, scene_data in scenes.items()
]
async def activate_scene_tool(client: HueClient, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""
Activate a scene.
Args:
arguments: Must contain 'scene_id'
Returns:
Success message
"""
scene_id = arguments.get("scene_id")
if not scene_id:
raise ValueError("scene_id is required")
await client.activate_scene(scene_id)
return {"success": True, "message": f"Scene {scene_id} activated"}
TOOLS = {
"list_lights": {
"function": list_lights_tool,
"description": "List all lights connected to the Hue Bridge with their current state",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
"get_light_state": {
"function": get_light_state_tool,
"description": "Get detailed state information for a specific light",
"parameters": {
"type": "object",
"properties": {
"light_id": {
"type": "string",
"description": "The unique identifier of the light",
}
},
"required": ["light_id"],
},
},
"turn_light_on": {
"function": turn_light_on_tool,
"description": "Turn a light on, optionally setting brightness",
"parameters": {
"type": "object",
"properties": {
"light_id": {
"type": "string",
"description": "The unique identifier of the light",
},
"brightness": {
"type": "number",
"description": "Optional brightness level (0-254)",
"minimum": 0,
"maximum": 254,
},
},
"required": ["light_id"],
},
},
"turn_light_off": {
"function": turn_light_off_tool,
"description": "Turn a light off",
"parameters": {
"type": "object",
"properties": {
"light_id": {
"type": "string",
"description": "The unique identifier of the light",
}
},
"required": ["light_id"],
},
},
"set_brightness": {
"function": set_brightness_tool,
"description": "Set the brightness level of a light",
"parameters": {
"type": "object",
"properties": {
"light_id": {
"type": "string",
"description": "The unique identifier of the light",
},
"brightness": {
"type": "number",
"description": "Brightness level (0-254, where 0 is minimum and 254 is maximum)",
"minimum": 0,
"maximum": 254,
},
},
"required": ["light_id", "brightness"],
},
},
"set_color_temp": {
"function": set_color_temp_tool,
"description": "Set the color temperature of a light in mireds (153=cold, 500=warm)",
"parameters": {
"type": "object",
"properties": {
"light_id": {
"type": "string",
"description": "The unique identifier of the light",
},
"color_temp": {
"type": "number",
"description": "Color temperature in mireds (153-500, where 153 is coldest and 500 is warmest)",
"minimum": 153,
"maximum": 500,
},
},
"required": ["light_id", "color_temp"],
},
},
"set_color": {
"function": set_color_tool,
"description": "Set the color of a light using CIE xy color space coordinates",
"parameters": {
"type": "object",
"properties": {
"light_id": {
"type": "string",
"description": "The unique identifier of the light",
},
"xy": {
"type": "array",
"description": "CIE xy color coordinates as [x, y], each value between 0 and 1",
"items": {"type": "number", "minimum": 0, "maximum": 1},
"minItems": 2,
"maxItems": 2,
},
},
"required": ["light_id", "xy"],
},
},
"list_groups": {
"function": list_groups_tool,
"description": "List all groups/rooms with their properties",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
"control_group": {
"function": control_group_tool,
"description": "Control all lights in a group/room at once",
"parameters": {
"type": "object",
"properties": {
"group_id": {
"type": "string",
"description": "The unique identifier of the group",
},
"on": {
"type": "boolean",
"description": "Turn lights on (true) or off (false)",
},
"brightness": {
"type": "number",
"description": "Optional brightness level (0-254)",
"minimum": 0,
"maximum": 254,
},
"color_temp": {
"type": "number",
"description": "Optional color temperature in mireds (153-500)",
"minimum": 153,
"maximum": 500,
},
"xy": {
"type": "array",
"description": "Optional CIE xy color coordinates as [x, y]",
"items": {"type": "number", "minimum": 0, "maximum": 1},
"minItems": 2,
"maxItems": 2,
},
},
"required": ["group_id"],
},
},
"list_scenes": {
"function": list_scenes_tool,
"description": "List all available scenes",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
"activate_scene": {
"function": activate_scene_tool,
"description": "Activate a predefined scene",
"parameters": {
"type": "object",
"properties": {
"scene_id": {
"type": "string",
"description": "The unique identifier of the scene",
}
},
"required": ["scene_id"],
},
},
}