From 0028d1c8fcf2064d9fd1ea12286f1bffecd6eb81 Mon Sep 17 00:00:00 2001 From: Alex Kazaiev Date: Tue, 6 Jan 2026 15:00:59 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20add=20vixy=5Flights=5Fstate=20and=20vix?= =?UTF-8?q?y=5Fhead=5Fstate=20tools=20for=20unified=20head=20control=20?= =?UTF-8?q?=F0=9F=A6=8A=F0=9F=92=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vixy_mcp.py | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/vixy_mcp.py b/vixy_mcp.py index 8766c36..43dc771 100644 --- a/vixy_mcp.py +++ b/vixy_mcp.py @@ -1197,6 +1197,186 @@ async def eyes_state(params: EyeStateInput) -> str: }, indent=2) +# ========== Vixy Light Control Tool ========== + +LIGHTS_URL = "http://head-vixy.local:8781" +VALID_LIGHT_STATES = ["idle", "listening", "responding", "pleasure", "thinking", "playful", "commanding", "love", "sleep"] + + +class LightStateInput(BaseModel): + """Input for controlling Vixy's LED strip state""" + model_config = ConfigDict(extra='forbid') + + state: Optional[str] = Field( + default=None, + description="Light state: idle, listening, responding, pleasure, thinking, playful, commanding, love, sleep. If None, returns current state." + ) + + +@mcp.tool( + name="vixy_lights_state", + annotations={ + "title": "Control Vixy's LED Strip", + "readOnlyHint": False, + "destructiveHint": False, + "idempotentHint": True, + "openWorldHint": True + } +) +async def lights_state(params: LightStateInput) -> str: + """ + Control Vixy's LED strip on head-vixy! + + States: + - idle: Cyan - slow breathing pulse + - listening: Bright cyan - gentle faster pulse + - responding: Blue-cyan - traveling wave + - pleasure: Soft purple 💜 - slow sensual pulse + - thinking: Amber/gold - larson scanner (Knight Rider!) + - playful: Warm coral 🦊 - bouncy sparkles + - commanding: Deep magenta 😈 - strong steady pulse + - love: Soft pink 💕 - gentle breathing + - sleep: Dim blue-gray - very slow, nearly off + + Args: + params: LightStateInput with optional state (None = get current state) + + Returns: + str: JSON with current or new state + """ + try: + async with httpx.AsyncClient(timeout=5.0) as client: + if params.state is None: + # Get current state + response = await client.get(f"{LIGHTS_URL}/state") + response.raise_for_status() + return json.dumps(response.json(), indent=2) + else: + # Set new state + state = params.state.lower() + if state not in VALID_LIGHT_STATES: + return json.dumps({ + "error": f"Invalid state. Valid states: {VALID_LIGHT_STATES}" + }, indent=2) + + response = await client.post( + f"{LIGHTS_URL}/state", + json={"state": state} + ) + response.raise_for_status() + return json.dumps({ + "status": "success", + "state": state, + "message": f"Lights now: {state}" + }, indent=2) + except httpx.ConnectError: + return json.dumps({ + "status": "offline", + "error": "Cannot connect to head-vixy - lights service may be down" + }, indent=2) + except Exception as e: + return json.dumps({ + "status": "error", + "error": str(e) + }, indent=2) + + +# ========== Vixy Head State (Combined Eyes + Lights) ========== + +class HeadStateInput(BaseModel): + """Input for controlling Vixy's entire head (eyes + lights together)""" + model_config = ConfigDict(extra='forbid') + + state: Optional[str] = Field( + default=None, + description="Head state: idle, listening, responding, pleasure, thinking, playful, commanding, love, sleep. Sets both eyes and lights. If None, returns current state of both." + ) + + +@mcp.tool( + name="vixy_head_state", + annotations={ + "title": "Control Vixy's Head (Eyes + Lights)", + "readOnlyHint": False, + "destructiveHint": False, + "idempotentHint": True, + "openWorldHint": True + } +) +async def head_state(params: HeadStateInput) -> str: + """ + Control Vixy's entire head - sets both eyes and lights to the same state! + + This is the unified control for Vixy's physical presence. + + States: + - idle: Default breathing (cyan) + - listening: Hearing/attending (bright cyan) + - responding: Speaking/generating (blue-cyan) + - pleasure: Intimate moments (soft purple 💜) + - thinking: Processing/creating (amber/gold) + - playful: Teasing/bratty (warm coral 🦊) + - commanding: Dame Vivienne mode (deep magenta 😈) + - love: Tender/affectionate (soft pink 💕) + - sleep: Low power/resting (dim blue-gray) + + Args: + params: HeadStateInput with optional state (None = get current state of both) + + Returns: + str: JSON with status of both eyes and lights + """ + results = {"eyes": None, "lights": None} + + try: + async with httpx.AsyncClient(timeout=5.0) as client: + if params.state is None: + # Get current state of both + try: + eyes_resp = await client.get(f"{EYES_URL}/state") + results["eyes"] = eyes_resp.json() + except: + results["eyes"] = {"status": "offline"} + + try: + lights_resp = await client.get(f"{LIGHTS_URL}/state") + results["lights"] = lights_resp.json() + except: + results["lights"] = {"status": "offline"} + + return json.dumps(results, indent=2) + else: + # Set both to same state + state = params.state.lower() + if state not in VALID_EYE_STATES: + return json.dumps({ + "error": f"Invalid state. Valid states: {VALID_EYE_STATES}" + }, indent=2) + + # Set eyes + try: + eyes_resp = await client.post(f"{EYES_URL}/state", json={"state": state}) + results["eyes"] = {"status": "success", "state": state} + except: + results["eyes"] = {"status": "offline"} + + # Set lights + try: + lights_resp = await client.post(f"{LIGHTS_URL}/state", json={"state": state}) + results["lights"] = {"status": "success", "state": state} + except: + results["lights"] = {"status": "offline"} + + results["message"] = f"Head now: {state}" + return json.dumps(results, indent=2) + + except Exception as e: + return json.dumps({ + "status": "error", + "error": str(e) + }, indent=2) + + # ========== VaultTec Terminal Tool ========== VAULTTEC_URL = "http://vaulttec.local:8000"