Skip to main content
Glama
00011110

trayfeel

by 00011110

TrayFeel

Plug-and-play CD/DVD tray emotive actuator for AI agents.

TrayFeel turns a Linux optical drive into a physical body-language channel: open, close, pulse, wiggle, and a catalog of named emotions — exposed as a Python library, CLI, MCP server, and optional HTTP daemon.

Linux-first. MIT licensed. Pure-Python ioctl path (no eject binary required).

Requirements

  • Python 3.10+

  • Linux with /dev/sr* optical drive supporting OPEN_TRAY / CLOSE_TRAY

  • User membership in the cdrom group (typical on Ubuntu/Debian)

sudo usermod -aG cdrom "$USER"
# log out and back in, then:
groups   # should list cdrom

Install

pip install trayfeel
# or from a checkout:
pip install -e ".[dev]"

Optional env (see .env.example):

Variable

Purpose

TRAYFEEL_DEVICE

Force device path (default: first /dev/sr*)

TRAYFEEL_TOKEN

Bearer token for HTTP daemon

TRAYFEEL_CAMERA

Optional /dev/videoN for vision feedback

TRAYFEEL_HOST / TRAYFEEL_PORT

Daemon bind (default 127.0.0.1:8765)

Quick start

trayfeel list-drives
trayfeel doctor
trayfeel status
trayfeel pulse
trayfeel emote laugh
trayfeel emote nod_yes --dry-run
trayfeel catalog

Python:

from trayfeel import Tray

with Tray() as tray:
    print(tray.status())
    tray.run("pulse")
    tray.emote("wave")

Safety

TrayFeel refuses reckless motion by default:

  • Exclusive flock per device (~/.cache/trayfeel/…)

  • Rate limit (default 30 actions/minute)

  • Minimum interval between motions (default 200 ms)

  • Disc guard — slam patterns (pulse, wiggle, nod, …) refused while a disc is present unless force=True

  • Cooldown after many open/close cycles

Override with --force / force=True when you intentionally accept the risk.

Emotions

Named choreographies in trayfeel/emotions/catalog.yaml:

laugh, giggle, gasp, surprise, shrug, think, ponder, nod_yes, shake_no, applause, sulk, pout, excited, bored, angry, curious, wave, heartbeat, sigh, celebrate, confused, acknowledge, reject, greet, farewell, error, success, idle_breath

trayfeel emote celebrate
trayfeel emote think --dry-run

Primitives

open, close, toggle, lock, unlock, status, pulse, wiggle, hold_open, nod, double_tap, spin_up, spin_down, sleep, wait

MCP server

trayfeel mcp
# or: python -m trayfeel mcp

Tools: tray_status, tray_list_drives, tray_primitive, tray_emote, tray_catalog, tray_doctor.

Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "trayfeel": {
      "command": "trayfeel",
      "args": ["mcp"]
    }
  }
}

Cursor

Add an MCP server entry pointing at the same command (trayfeel mcp or python -m trayfeel mcp). Ensure the environment has cdrom group permissions.

Tool schemas for OpenAI / Claude function calling live in examples/openai_tools.json and examples/claude_tools.json. Agent skill notes: examples/agent_skill.md.

HTTP daemon

For LAN agents that cannot attach stdio MCP:

export TRAYFEEL_TOKEN="$(openssl rand -hex 16)"
trayfeel daemon --host 0.0.0.0 --port 8765

Method

Path

Body

GET

/health

GET

/status

GET

/catalog

POST

/primitive

{"name":"pulse","open_ms":300}

POST

/emote

{"name":"wave"}

Send Authorization: Bearer $TRAYFEEL_TOKEN when a token is configured.

Vision (optional)

If TRAYFEEL_CAMERA=/dev/video0 is set and ffmpeg is installed, trayfeel vision captures a frame via V4L2 and returns a best-effort brightness/edge heuristic. Missing camera or ffmpeg returns ok: false with a reason — it never crashes the process.

Library layout

trayfeel/
  core.py          # Tray, Emoter
  primitives.py    # motion primitives
  safety.py        # flock, rate limits, disc guards
  hardware/linux.py
  emotions/        # catalog.yaml + engine
  mcp_server.py
  daemon.py
  cli.py

License

MIT — Copyright (c) 2026 TrayFeel contributors

Related MCP Connectors