feat: add vixy_lights_state and vixy_head_state tools for unified head control 🦊💡

This commit is contained in:
Alex Kazaiev
2026-01-06 15:00:59 -06:00
parent 6e43482636
commit 0028d1c8fc

View File

@@ -1197,6 +1197,186 @@ async def eyes_state(params: EyeStateInput) -> str:
}, indent=2) }, 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 Terminal Tool ==========
VAULTTEC_URL = "http://vaulttec.local:8000" VAULTTEC_URL = "http://vaulttec.local:8000"