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)
|
}, 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user