feat: add vixy_lights_state and vixy_head_state tools for unified head control 🦊💡
This commit is contained in:
180
vixy_mcp.py
180
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"
|
||||
|
||||
Reference in New Issue
Block a user