Skip to main content
Glama

tuya-local-mcp

Zero-cloud LAN controller for Tuya / Smart Life bulbs: an MCP server for Claude Code plus a TrueColor CLI.

$ l focus
[FOCUS] ceiling: off | setup: 85% · temp 90 | bed: 15% · temp 0  312 ms
  • Local only. Commands go straight to the bulbs over your Wi-Fi (TCP 6668). The Tuya cloud is used once, to download each bulb's key, and never again.

  • A CLI you'll actually use. l toggles the room, l bed 20 dims the bed lamp, l sleep 30m turns everything off later. Output is painted in the colour the bulb shows.

  • Works in Claude Desktop too, on any plan, including ambient mode.

  • Claude Code integration. A /light command that understands "fireplace vibe on the desk in 10 minutes". Lights turn red when your tests fail and flash green when they pass again. An optional ambient mode lets Claude set the mood while it works.


Contents


Related MCP server: io.github.jonnyblankinship/hue-ble-mcp

How it works

One program, two ways in. When you type a command, it's a CLI. When Claude Code starts it (no terminal attached), it's an MCP server. Both share the same engine:

   you, in a terminal                     Claude Code
   ──────────────────                     ───────────
   l bed night                            /light "reading in bed"
         │                                 │            │
         │                          MCP tools (stdio)   hooks: test failed / passed
         ▼                                 ▼            ▼
   ┌────────────────────────────────────────────────────────────┐
   │ tuya-local-mcp                                             │
   │                                                            │
   │   cli.py ─┐                     ┌─ server.py   signals.py  │
   │           ▼                     ▼                          │
   │      commands.py  ── presets.json, devices.json            │
   │           │          (in ~/.config/tuya-local-mcp)         │
   │           ▼                                                │
   │      engine.py: one connection per light, all in parallel  │
   │        • builds the whole change as ONE message            │
   │        • light unreachable? listen for its LAN broadcast,  │
   │          save the new IP, retry                            │
   └───────────┬───────────────────────┬────────────────────────┘
               │ TCP 6668              │ UDP 6666/6667 (discovery)
               ▼                       ▼
        ┌────────────┐  ┌────────────┐  ┌────────────┐
        │  ceiling   │  │   setup    │  │    bed     │   your Wi-Fi bulbs
        └────────────┘  └────────────┘  └────────────┘

Delayed commands (l sleep 30m, or Claude's delay_seconds) run in a small background process, so they still fire after you close the terminal or Claude Code.

Requirements

  • Tuya-based Wi-Fi bulbs that work with the Smart Life or Tuya Smart app. Many cheap "smart bulbs" are Tuya inside, and the app tells you: if it's Smart Life, it works.

  • The computer on the same local network as the bulbs.

  • Python 3.10 or newer and uv.

  • A free Tuya IoT developer account, used once to download the keys (step 2).

  • For the Claude Code features: Claude Code.

Setup

1. Install the command

uv tool install git+https://github.com/meow-dot-py/tuya-local-mcp

This puts tuya-local-mcp on your PATH. Check it with tuya-local-mcp help.

To update later, run uv tool upgrade tuya-local-mcp.

2. Get your bulbs' local keys (once)

Each bulb encrypts its local traffic with its own key. The only way to get that key is through Tuya's developer cloud. You do this once, and again only if you re-pair a bulb or add new ones.

a. Create a cloud project

  1. Sign up at platform.tuya.com (free).

  2. Go to Cloud → Development → Create Cloud Project.

    • Development method: Smart Home.

    • Data center: the one your app account uses. If you're unsure, it's usually the closest to you: Central Europe, Western America, Eastern America, India or China.

  3. When asked for API services, keep the defaults. Only two are needed: IoT Core and Authorization Token Management. The others the template adds (Smart Home Basic Service, Device Status Notification, ...) are harmless. You can check them later under the project's Service API tab.

  4. On the project's Overview tab, copy the Access ID and Access Secret.

NOTE

IoT Core is a free trial that expires after about a month. It's only needed while the wizard downloads keys, so the lights keep working after it expires. When you add or re-pair a bulb later, renew it first:Service API → IoT Core → Extend trial period (free, after a short questionnaire).

b. Link your Smart Life account

  1. In the project, open Devices → Link App Account → Add App Account. A QR code appears.

  2. In the Smart Life app, go to Me → the scan icon (top right), and scan it.

  3. Your bulbs now appear under Devices. Copy any one Device ID.

c. Run the wizard

tuya-local-mcp init

It runs the tinytuya wizard inside the config folder (~/.config/tuya-local-mcp) and asks for:

Prompt

Answer

API Key

the Access ID

API Secret

the Access Secret

any Device ID

the Device ID you copied

Your Region

eu, us, us-e, in or cn, matching your data center

Download DP Name mappings?

Y. This lets each change go out as a single message

Poll local devices?

Y. This finds the IPs

IMPORTANT

Force-close the Smart Life app on your phone before polling and whenever you use the lights from here. Many bulbs accept only one local connection, and the app may be holding it.

When it finishes, you'll see your lights listed. The wizard also leaves tinytuya.json, tuya-raw.json and snapshot.json in the config folder. They contain secrets: never share or commit them.

Copy devices.example.json to ~/.config/tuya-local-mcp/devices.json and fill in each bulb's id, key, ip and version (3.3, 3.4 or 3.5). Without the wizard's mapping, the first command to each bulb takes one extra round trip to detect its type.

3. Try it

tuya-local-mcp status
tuya-local-mcp presets

Then give it a short name. In PowerShell (notepad $PROFILE):

function light { tuya-local-mcp @args }
Set-Alias -Name l -Value light

In bash / zsh (~/.bashrc or ~/.zshrc):

alias l='tuya-local-mcp'
alias light='tuya-local-mcp'

From here on, the examples use l.

4. Add it to Claude Code

The plugin (recommended) gives you everything: the MCP tools, /light, the test/build signals and ambient mode. Inside Claude Code:

/plugin marketplace add meow-dot-py/tuya-local-mcp
/plugin install tuya-local@tuya-local-mcp

Then open /config to adjust the plugin's options (see Claude Code features).

MCP tools only, if you don't want the hooks or skill:

claude mcp add tuya-local -s user -- tuya-local-mcp serve
NOTE

Use one or the other. The plugin already includes the MCP server, so registering both gives Claude every tool twice.

4b. Or add it to Claude Desktop (any plan, including free)

The Claude desktop app can use the MCP server in normal chats, so you can say "make it cosy in here" or "lights off in 20 minutes" without Claude Code.

  1. Find the full path of the command. In PowerShell: (Get-Command tuya-local-mcp).Source. In bash/zsh: which tuya-local-mcp.

  2. In Claude Desktop, open Settings → Developer → Edit Config and add the server to claude_desktop_config.json:

    {
      "mcpServers": {
        "tuya-local": {
          "command": "C:\\Users\\you\\.local\\bin\\tuya-local-mcp.exe",
          "args": ["serve"],
          "env": { "TUYA_LOCAL_MCP_AMBIENT": "false" }
        }
      }
    }

    Use the full path from step 1 (on Windows, double each backslash). The desktop app doesn't always see the same PATH as your terminal.

    Can't find the menu? Edit the file directly. Usually it's %APPDATA%\Claude\claude_desktop_config.json on Windows and ~/Library/Application Support/Claude/claude_desktop_config.json on macOS. Microsoft Store installs may keep it elsewhere, such as %LOCALAPPDATA%\Claude-Data\claude_<id>\. This PowerShell command finds it: Get-ChildItem $env:APPDATA, $env:LOCALAPPDATA -Recurse -Depth 3 -Filter claude_desktop_config.json. Add mcpServers next to whatever is already in the file.

  3. Fully quit Claude Desktop (including from the system tray) and reopen it. tuya-local now appears under + → Connectors.

  4. Make sure its switch is on there, and under + → Connectors → Tool access allow its tools (or choose Always allow the first time Claude uses one). Otherwise every light change, including ambient ones, waits for your approval.

Ambient mode works here too, in two ways:

  • Always on: set TUYA_LOCAL_MCP_AMBIENT to "true" in the config above. Every chat starts with the ambient instructions, and Claude reads the local time from light_status when the hour matters.

  • For one chat: + → Connectors → Add from tuya-local → Ambient mode attaches the instructions, with the current time, to your message. Use it when the variable is off, or to refresh the time in a long-running chat.

The chat may load connector tools only when something in the conversation points to them, so Claude might not think of the lights unless you hint at them ("set the mood, I'm going to game"). To have it always aware, paste one line into Settings → Profile → personal preferences:

My room lights can be controlled with the tuya-local tools: set the mood on your own when the topic changes (work/study: focus, games: gaming, late night: chill).

Using both the plugin and this config entry? The desktop app also hands this server to its Code tab sessions, so the plugin notices it and steps aside there: its own copy of the server registers no tools, and its ambient hook stays quiet if the entry above has ambient on. You get each tool once. The terminal claude CLI doesn't read this file, so it keeps using the plugin's copy.

What you don't get outside Claude Code: the /light shortcut (you don't need it, just ask) and the red/green test signals (there are no commands to watch in a chat).

Using the CLI

l                         toggle: any light on -> all off, otherwise all on
l [target] on|off|toggle
l [target] <preset>       l focus · l bed night
l [target] <bri> [temp]   l 40 · l setup 85 90   (brightness 1-100, temp 0 warm - 100 cool)
l [target] rgb R G B [bri]   l rgb 255 0 128
l [target] sleep <time> [command]   l sleep 30m · l bed sleep 1h night
l timers                  pending timers, with their ids
l cancel [id]             cancel one timer, or all of them
l [target] status

l presets                 your presets, with colour previews
l lights                  your lights and every name that targets them
l scan                    look for lights whose IP changed (also automatic on errors)
l init                    the one-time key download (step 2)
l signal fail|pass        try the red/green build signal by hand

Durations accept 90, 45s, 10m, 1h30m or 1.5h. Timers survive closing the terminal or Claude, and each one gets a short id you can cancel it by ([TIMER 8ee7] ... cancel: l cancel 8ee7). Out-of-range numbers are clamped with a warning. A mistyped word gets a suggestion ('focsu' ... Did you mean: focus?). The exit code is 0 on success, 1 if any light failed and 2 for a usage error, so the CLI works in scripts.

Colours turn off automatically when the output isn't a terminal or NO_COLOR is set.

Lights, zones and targets

A target picks lights. It can be:

Target

Picks

all (default)

every light

a zone

lights with that "zone" in devices.json

a full name

"desk lamp" matches Desk Lamp

a word of a name

desk or lamp matches Desk Lamp

bed,setup

several, comma-separated

Matching is whole-word and case-insensitive, so s never matches by accident. An unknown target is an error that lists the valid ones; it never quietly picks some other light.

Give your bulbs short zone names by adding "zone" to their entries in ~/.config/tuya-local-mcp/devices.json:

{ "name": "Lámpara de noche", "zone": "bed", "id": "...", "key": "...", "ip": "192.168.1.52", "version": "3.3" }

Presets and scenes

Presets live in ~/.config/tuya-local-mcp/presets.json and are re-read on every command, so edits apply at once. Until you save your own, a built-in set is used (code, chill, night, cyber, red, focus, gaming, movie).

A preset is one setting for whatever you target:

{
  "chill":   { "temp": 0, "brightness": 20, "description": "Dim, warm and relaxed" },
  "cyber":   { "rgb": [255, 0, 255], "brightness": 90 }
}

A scene gives each target its own setting:

{
  "focus": {
    "targets": {
      "ceiling": { "on": false },
      "setup":   { "temp": 90, "brightness": 85 },
      "bed":     { "temp": 0,  "brightness": 15 }
    }
  }
}

Settings accept on, brightness (1-100), temp (0-100) and rgb ([r, g, b], 0-255). Use either temp (white) or rgb (colour), not both. A target narrows a scene: l bed focus applies only focus's bed setting. Scene targets that match none of your lights are skipped and reported.

A broken preset is skipped with a warning; it never takes the others down with it. Claude can also create, edit and delete presets for you.

Claude Code features

/light

/light focus
/light bed 20
/light fireplace vibe on the desk
/light apaga todo en media hora
/light save this as "reading": warm, 60%

It maps any phrasing, in any language, to a single tool call, with no status lookups first, and replies with one line in the CLI's format. Plugin skills can also be invoked as /tuya-local:light.

Build and test signals

What happens

The lights

A test or build command Claude runs fails

turn red (their previous state is saved)

It passes again

flash green for 4 s, then go back to exactly how they were

It passes and nothing had failed

don't change, so green runs don't flicker

This uses Claude Code hooks, so it works every time and costs no tokens. Recognised commands include pytest, tox, jest, vitest, npm|pnpm|yarn|bun test/build, cargo, go, make, gradle, mvn, dotnet, tsc, mypy, pyright, swift, xcodebuild and docker build. If a red state is older than 3 hours, the next pass leaves your lights alone instead of restoring something stale.

Ambient mode

When enabled, Claude gets a short instruction at the start of each session: shift to focus during long debugging, to cyber or gaming for creative work, and to chill or night late at night. It does this at most once per task, silently, with at most a [LIGHT: focus] tag at the end of a reply. It's off by default.

Options (/config → tuya-local)

Option

Default

Build/test signals

on

turn the red/green signals on or off

Signal lights

all

which target shows them, e.g. setup

Signal commands (regex)

built-in list

your own regular expression for "test or build" commands

Ambient mood

off

let Claude set the mood on its own

Outside Claude Code, see Claude Desktop for ambient mode. Without the plugin, set the same options as environment variables: TUYA_LOCAL_MCP_SIGNALS, TUYA_LOCAL_MCP_SIGNAL_TARGET, TUYA_LOCAL_MCP_SIGNAL_COMMANDS and TUYA_LOCAL_MCP_AMBIENT. To use the hooks without the plugin, copy the entries from plugin/hooks/hooks.json into your settings.json.

MCP tools

Tool

Does

power_light(state, target, delay_seconds)

on / off / group toggle

set_light(target, preset | brightness, temp, rgb, delay_seconds)

apply a preset or scene, or set values

light_status(target)

what each light is doing now, plus the local time

list_lights()

lights, zones, the names that target them, IPs, presets

get_presets() / upsert_preset(name, config) / delete_preset(name)

manage presets.json, with validation

list_timers() / cancel_timer(timer_id | "all")

see and cancel pending timers

scan_lan_ips()

find moved lights and save their new IPs

The server also offers an ambient_mode prompt that switches ambient mode on for one conversation, in any client that supports MCP prompts.

Every result includes a one-line summary and per-light ok/error, so a failure is never reported as success.

Troubleshooting

A light is "unreachable". It's probably one of these:

  1. The Smart Life app is open on a phone and holding the bulb's only local connection. Force-close it.

  2. The bulb is switched off at the wall. Bulbs can't be reached without power.

  3. Its IP changed. This is fixed automatically (the light is found by its broadcast and the new IP saved). You can also run l scan.

  4. Firewall. Allow Python to receive UDP 6666/6667 (discovery) and connect to TCP 6668.

"Check device key or version". The bulb was re-paired in the app, which gives it a new key. Run tuya-local-mcp init again.

The signals don't fire. Check ~/.config/tuya-local-mcp/hook.log. Every signal and every error is logged there. Make sure tuya-local-mcp is on the PATH that Claude Code sees, and try l signal fail / l signal pass by hand.

How fast is it? About 0.3-0.5 s per command, end to end. Most of that is Python starting and the bulb acknowledging the message (~200 ms on typical bulbs). Through the MCP server the process is already running, so only the bulb's time is left.

Security

  • Bulb keys and Tuya cloud credentials stay in ~/.config/tuya-local-mcp/, outside any repository. Set TUYA_LOCAL_MCP_HOME to use another folder.

  • Files there are written atomically, so a crash can't corrupt them.

  • After the one-time setup, nothing talks to the internet. You can even revoke the Tuya cloud project.

  • This repository's .gitignore excludes every file the wizard creates, in case you run it in the repo by mistake.

Development

git clone https://github.com/meow-dot-py/tuya-local-mcp
cd tuya-local-mcp
uv sync
uv run pytest           # uses fake bulbs; no hardware or network needed
uv run ruff check . && uv run ruff format --check .

To use your working copy everywhere while you hack on it: uv tool install --editable .

To test the plugin from the repo without installing it: claude --plugin-dir ./plugin.

Each module has one job and a docstring that explains it. Start with src/tuya_local_mcp/__init__.py for the map.

License

MIT © 2026 Johann 'M' Nestorida. Built on tinytuya and the MCP Python SDK. Not affiliated with Tuya.

Related MCP Connectors

Related MCP Servers