tuya-local-mcp 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. tuya_local_mcp-0.3.0/.claude-plugin/marketplace.json +17 -0
  2. tuya_local_mcp-0.3.0/.gitattributes +2 -0
  3. tuya_local_mcp-0.3.0/.github/workflows/ci.yml +21 -0
  4. tuya_local_mcp-0.3.0/.github/workflows/publish.yml +41 -0
  5. tuya_local_mcp-0.3.0/.gitignore +29 -0
  6. tuya_local_mcp-0.3.0/LICENSE +21 -0
  7. tuya_local_mcp-0.3.0/PKG-INFO +416 -0
  8. tuya_local_mcp-0.3.0/README.md +397 -0
  9. tuya_local_mcp-0.3.0/devices.example.json +26 -0
  10. tuya_local_mcp-0.3.0/plugin/.claude-plugin/plugin.json +40 -0
  11. tuya_local_mcp-0.3.0/plugin/.mcp.json +8 -0
  12. tuya_local_mcp-0.3.0/plugin/hooks/hooks.json +29 -0
  13. tuya_local_mcp-0.3.0/plugin/skills/light/SKILL.md +67 -0
  14. tuya_local_mcp-0.3.0/pyproject.toml +48 -0
  15. tuya_local_mcp-0.3.0/src/tuya_local_mcp/__init__.py +21 -0
  16. tuya_local_mcp-0.3.0/src/tuya_local_mcp/__main__.py +5 -0
  17. tuya_local_mcp-0.3.0/src/tuya_local_mcp/_tinytuya.py +32 -0
  18. tuya_local_mcp-0.3.0/src/tuya_local_mcp/ambient.py +66 -0
  19. tuya_local_mcp-0.3.0/src/tuya_local_mcp/cli.py +365 -0
  20. tuya_local_mcp-0.3.0/src/tuya_local_mcp/commands.py +212 -0
  21. tuya_local_mcp-0.3.0/src/tuya_local_mcp/config.py +73 -0
  22. tuya_local_mcp-0.3.0/src/tuya_local_mcp/desktop.py +193 -0
  23. tuya_local_mcp-0.3.0/src/tuya_local_mcp/devices.py +205 -0
  24. tuya_local_mcp-0.3.0/src/tuya_local_mcp/engine.py +203 -0
  25. tuya_local_mcp-0.3.0/src/tuya_local_mcp/presets.py +194 -0
  26. tuya_local_mcp-0.3.0/src/tuya_local_mcp/protocol.py +218 -0
  27. tuya_local_mcp-0.3.0/src/tuya_local_mcp/render.py +125 -0
  28. tuya_local_mcp-0.3.0/src/tuya_local_mcp/server.py +257 -0
  29. tuya_local_mcp-0.3.0/src/tuya_local_mcp/signals.py +231 -0
  30. tuya_local_mcp-0.3.0/src/tuya_local_mcp/state.py +141 -0
  31. tuya_local_mcp-0.3.0/src/tuya_local_mcp/timers.py +199 -0
  32. tuya_local_mcp-0.3.0/tests/__init__.py +0 -0
  33. tuya_local_mcp-0.3.0/tests/conftest.py +117 -0
  34. tuya_local_mcp-0.3.0/tests/test_ambient.py +46 -0
  35. tuya_local_mcp-0.3.0/tests/test_behaviour.py +168 -0
  36. tuya_local_mcp-0.3.0/tests/test_bugs.py +91 -0
  37. tuya_local_mcp-0.3.0/tests/test_desktop.py +96 -0
  38. tuya_local_mcp-0.3.0/tests/test_signals.py +144 -0
  39. tuya_local_mcp-0.3.0/tests/test_timers.py +85 -0
  40. tuya_local_mcp-0.3.0/uv.lock +1475 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "tuya-local-mcp",
3
+ "owner": {
4
+ "name": "Johann 'M' Nestorida",
5
+ "url": "https://github.com/meow-dot-py"
6
+ },
7
+ "metadata": {
8
+ "description": "Control Tuya / Smart Life lights from Claude Code over your LAN."
9
+ },
10
+ "plugins": [
11
+ {
12
+ "name": "tuya-local",
13
+ "source": "./plugin",
14
+ "description": "Room lights for Claude Code: /light command, red/green build and test signals, optional ambient mood. Needs the tuya-local-mcp command installed."
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,2 @@
1
+ * text=auto eol=lf
2
+ *.ps1 text eol=crlf
@@ -0,0 +1,21 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, windows-latest, macos-latest]
14
+ runs-on: ${{ matrix.os }}
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ - run: uv sync
19
+ - run: uv run ruff check .
20
+ - run: uv run ruff format --check .
21
+ - run: uv run pytest -q
@@ -0,0 +1,41 @@
1
+ # Publishes to PyPI when a GitHub release is published.
2
+ # Uses PyPI Trusted Publishing: no API token is stored anywhere. One-time setup on
3
+ # pypi.org -> Your projects / Publishing -> add a (pending) GitHub publisher:
4
+ # project tuya-local-mcp, owner meow-dot-py, repository tuya-local-mcp,
5
+ # workflow publish.yml, environment pypi
6
+ name: Publish to PyPI
7
+
8
+ on:
9
+ release:
10
+ types: [published]
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ - run: uv sync
19
+ - run: uv run pytest -q
20
+ - name: Check the tag matches the package version
21
+ run: |
22
+ version=$(uv run python -c "import tuya_local_mcp; print(tuya_local_mcp.__version__)")
23
+ test "v$version" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} != v$version"; exit 1; }
24
+ - run: uv build
25
+ - uses: actions/upload-artifact@v4
26
+ with:
27
+ name: dist
28
+ path: dist/
29
+
30
+ publish:
31
+ needs: build
32
+ runs-on: ubuntu-latest
33
+ environment: pypi
34
+ permissions:
35
+ id-token: write
36
+ steps:
37
+ - uses: actions/download-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,29 @@
1
+ # --- Secrets: never commit these -------------------------------------------
2
+ # `tinytuya wizard` writes your Tuya Cloud API credentials and every bulb's
3
+ # local_key into these files. tuya-local-mcp keeps them in its config folder
4
+ # (~/.config/tuya-local-mcp), but ignore them here too in case someone runs the
5
+ # wizard from inside the repository.
6
+ devices.json
7
+ tinytuya.json
8
+ tuya-raw.json
9
+ snapshot.json
10
+ presets.json
11
+ *.local.json
12
+
13
+ # --- Python ------------------------------------------------------------------
14
+ __pycache__/
15
+ *.py[cod]
16
+ *.egg-info/
17
+ build/
18
+ dist/
19
+ .venv/
20
+ .pytest_cache/
21
+ .ruff_cache/
22
+ .coverage
23
+ htmlcov/
24
+
25
+ # --- Editors / OS ------------------------------------------------------------
26
+ .vscode/
27
+ .idea/
28
+ .DS_Store
29
+ Thumbs.db
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Johann 'M' Nestorida
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,416 @@
1
+ Metadata-Version: 2.5
2
+ Name: tuya-local-mcp
3
+ Version: 0.3.0
4
+ Summary: Zero-cloud LAN controller for Tuya / Smart Life bulbs: an MCP server for Claude Code plus a TrueColor CLI.
5
+ Project-URL: Homepage, https://github.com/meow-dot-py/tuya-local-mcp
6
+ Project-URL: Issues, https://github.com/meow-dot-py/tuya-local-mcp/issues
7
+ Author: Johann 'M' Nestorida
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: claude-code,lights,mcp,smart-home,smart-life,tinytuya,tuya
11
+ Classifier: Environment :: Console
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Home Automation
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: mcp>=2.0
17
+ Requires-Dist: tinytuya>=1.17
18
+ Description-Content-Type: text/markdown
19
+
20
+ # tuya-local-mcp
21
+
22
+ [![PyPI](https://img.shields.io/pypi/v/tuya-local-mcp?style=flat)](https://pypi.org/project/tuya-local-mcp/)
23
+ [![CI](https://github.com/meow-dot-py/tuya-local-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/meow-dot-py/tuya-local-mcp/actions/workflows/ci.yml)
24
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)
25
+ [![MCP Badge](https://lobehub.com/badge/mcp/meow-dot-py-tuya-local-mcp?style=flat)](https://lobehub.com/mcp/meow-dot-py-tuya-local-mcp)
26
+
27
+ Zero-cloud LAN controller for Tuya / Smart Life bulbs: an MCP server for Claude Code plus a TrueColor CLI.
28
+
29
+ ```text
30
+ $ l focus
31
+ [FOCUS] ceiling: off | setup: 85% · temp 90 | bed: 15% · temp 0 312 ms
32
+ ```
33
+
34
+ - **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.
35
+ - **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.
36
+ - **Works in Claude Desktop too, on any plan**, including ambient mode.
37
+ - **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.
38
+
39
+ ---
40
+
41
+ ## Contents
42
+
43
+ - [How it works](#how-it-works)
44
+ - [Requirements](#requirements)
45
+ - [Setup](#setup)
46
+ 1. [Install the command](#1-install-the-command)
47
+ 2. [Get your bulbs' local keys (once)](#2-get-your-bulbs-local-keys-once)
48
+ 3. [Try it](#3-try-it)
49
+ 4. [Add it to Claude Code](#4-add-it-to-claude-code), or [to Claude Desktop](#4b-or-add-it-to-claude-desktop-any-plan-including-free)
50
+ - [Using the CLI](#using-the-cli)
51
+ - [Lights, zones and targets](#lights-zones-and-targets)
52
+ - [Presets and scenes](#presets-and-scenes)
53
+ - [Claude Code features](#claude-code-features)
54
+ - [Troubleshooting](#troubleshooting)
55
+ - [Security](#security)
56
+ - [Development](#development)
57
+
58
+ ---
59
+
60
+ ## How it works
61
+
62
+ 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:
63
+
64
+ ```text
65
+ you, in a terminal Claude Code
66
+ ────────────────── ───────────
67
+ l bed night /light "reading in bed"
68
+ │ │ │
69
+ │ MCP tools (stdio) hooks: test failed / passed
70
+ ▼ ▼ ▼
71
+ ┌────────────────────────────────────────────────────────────┐
72
+ │ tuya-local-mcp │
73
+ │ │
74
+ │ cli.py ─┐ ┌─ server.py signals.py │
75
+ │ ▼ ▼ │
76
+ │ commands.py ── presets.json, devices.json │
77
+ │ │ (in ~/.config/tuya-local-mcp) │
78
+ │ ▼ │
79
+ │ engine.py: one connection per light, all in parallel │
80
+ │ • builds the whole change as ONE message │
81
+ │ • light unreachable? listen for its LAN broadcast, │
82
+ │ save the new IP, retry │
83
+ └───────────┬───────────────────────┬────────────────────────┘
84
+ │ TCP 6668 │ UDP 6666/6667 (discovery)
85
+ ▼ ▼
86
+ ┌────────────┐ ┌────────────┐ ┌────────────┐
87
+ │ ceiling │ │ setup │ │ bed │ your Wi-Fi bulbs
88
+ └────────────┘ └────────────┘ └────────────┘
89
+ ```
90
+
91
+ 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.
92
+
93
+ ## Requirements
94
+
95
+ - 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.
96
+ - The computer on the same local network as the bulbs.
97
+ - Python 3.10 or newer and [uv](https://docs.astral.sh/uv/getting-started/installation/).
98
+ - A free Tuya IoT developer account, used once to download the keys (step 2).
99
+ - For the Claude Code features: [Claude Code](https://code.claude.com).
100
+
101
+ ## Setup
102
+
103
+ ### 1. Install the command
104
+
105
+ ```bash
106
+ uv tool install tuya-local-mcp
107
+ ```
108
+
109
+ This puts `tuya-local-mcp` on your PATH. Check it with `tuya-local-mcp help`.
110
+
111
+ To update later, run `uv tool upgrade tuya-local-mcp`. Close Claude Desktop / Claude Code first and cancel pending timers (`l cancel`): they keep the program's files in use, and the update can't replace them. The latest unreleased code installs with `uv tool install git+https://github.com/meow-dot-py/tuya-local-mcp`.
112
+
113
+ ### 2. Get your bulbs' local keys (once)
114
+
115
+ 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.
116
+
117
+ **a. Create a cloud project**
118
+
119
+ 1. Sign up at [platform.tuya.com](https://platform.tuya.com) (free).
120
+ 2. Go to **Cloud → Development → Create Cloud Project**.
121
+ - Development method: **Smart Home**.
122
+ - 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*.
123
+ 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.
124
+ 4. On the project's **Overview** tab, copy the **Access ID** and **Access Secret**.
125
+
126
+ > [!NOTE]
127
+ > 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).
128
+
129
+ **b. Link your Smart Life account**
130
+
131
+ 1. In the project, open **Devices → Link App Account → Add App Account**. A QR code appears.
132
+ 2. In the Smart Life app, go to **Me →** the scan icon (top right), and scan it.
133
+ 3. Your bulbs now appear under **Devices**. Copy any one **Device ID**.
134
+
135
+ **c. Run the wizard**
136
+
137
+ ```bash
138
+ tuya-local-mcp init
139
+ ```
140
+
141
+ It runs the [tinytuya](https://github.com/jasonacox/tinytuya) wizard inside the config folder (`~/.config/tuya-local-mcp`) and asks for:
142
+
143
+ | Prompt | Answer |
144
+ |---|---|
145
+ | API Key | the Access ID |
146
+ | API Secret | the Access Secret |
147
+ | any Device ID | the Device ID you copied |
148
+ | Your Region | `eu`, `us`, `us-e`, `in` or `cn`, matching your data center |
149
+ | Download DP Name mappings? | **Y**. This lets each change go out as a single message |
150
+ | Poll local devices? | **Y**. This finds the IPs |
151
+
152
+ > [!IMPORTANT]
153
+ > **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.
154
+
155
+ 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.**
156
+
157
+ <details>
158
+ <summary>Prefer to fill in devices.json by hand?</summary>
159
+
160
+ Copy [`devices.example.json`](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.
161
+ </details>
162
+
163
+ ### 3. Try it
164
+
165
+ ```bash
166
+ tuya-local-mcp status
167
+ tuya-local-mcp presets
168
+ ```
169
+
170
+ Then give it a short name. In **PowerShell** (`notepad $PROFILE`):
171
+
172
+ ```powershell
173
+ function light { tuya-local-mcp @args }
174
+ Set-Alias -Name l -Value light
175
+ ```
176
+
177
+ In **bash / zsh** (`~/.bashrc` or `~/.zshrc`):
178
+
179
+ ```bash
180
+ alias l='tuya-local-mcp'
181
+ alias light='tuya-local-mcp'
182
+ ```
183
+
184
+ From here on, the examples use `l`.
185
+
186
+ ### 4. Add it to Claude Code
187
+
188
+ **The plugin (recommended)** gives you everything: the MCP tools, `/light`, the test/build signals and ambient mode. Inside Claude Code:
189
+
190
+ ```text
191
+ /plugin marketplace add meow-dot-py/tuya-local-mcp
192
+ /plugin install tuya-local@tuya-local-mcp
193
+ ```
194
+
195
+ Then open `/config` to adjust the plugin's options (see [Claude Code features](#claude-code-features)).
196
+
197
+ **MCP tools only**, if you don't want the hooks or skill:
198
+
199
+ ```bash
200
+ claude mcp add tuya-local -s user -- tuya-local-mcp serve
201
+ ```
202
+
203
+ > [!NOTE]
204
+ > Use one or the other. The plugin already includes the MCP server, so registering both gives Claude every tool twice.
205
+
206
+ ### 4b. Or add it to Claude Desktop (any plan, including free)
207
+
208
+ 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.
209
+
210
+ 1. Find the full path of the command. In PowerShell: `(Get-Command tuya-local-mcp).Source`. In bash/zsh: `which tuya-local-mcp`.
211
+ 2. In Claude Desktop, open **Settings → Developer → Edit Config** and add the server to `claude_desktop_config.json`:
212
+
213
+ ```json
214
+ {
215
+ "mcpServers": {
216
+ "tuya-local": {
217
+ "command": "C:\\Users\\you\\.local\\bin\\tuya-local-mcp.exe",
218
+ "args": ["serve"],
219
+ "env": { "TUYA_LOCAL_MCP_AMBIENT": "false" }
220
+ }
221
+ }
222
+ }
223
+ ```
224
+
225
+ 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.
226
+
227
+ 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.
228
+ 3. Fully quit Claude Desktop (including from the system tray) and reopen it. **tuya-local** now appears under **+ → Connectors**.
229
+ 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.
230
+
231
+ Ambient mode works here too, in two ways:
232
+
233
+ - **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.
234
+ - **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.
235
+
236
+ 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**:
237
+
238
+ ```text
239
+ 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).
240
+ ```
241
+
242
+ 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.
243
+
244
+ 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).
245
+
246
+ ## Using the CLI
247
+
248
+ ```text
249
+ l toggle: any light on -> all off, otherwise all on
250
+ l [target] on|off|toggle
251
+ l [target] <preset> l focus · l bed night
252
+ l [target] <bri> [temp] l 40 · l setup 85 90 (brightness 1-100, temp 0 warm - 100 cool)
253
+ l [target] rgb R G B [bri] l rgb 255 0 128
254
+ l [target] sleep <time> [command] l sleep 30m · l bed sleep 1h night
255
+ l timers pending timers, with their ids
256
+ l cancel [id] cancel one timer, or all of them
257
+ l [target] status
258
+
259
+ l presets your presets, with colour previews
260
+ l lights your lights and every name that targets them
261
+ l scan look for lights whose IP changed (also automatic on errors)
262
+ l init the one-time key download (step 2)
263
+ l signal fail|pass try the red/green build signal by hand
264
+ ```
265
+
266
+ 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.
267
+
268
+ Colours turn off automatically when the output isn't a terminal or `NO_COLOR` is set.
269
+
270
+ ## Lights, zones and targets
271
+
272
+ A **target** picks lights. It can be:
273
+
274
+ | Target | Picks |
275
+ |---|---|
276
+ | `all` (default) | every light |
277
+ | a **zone** | lights with that `"zone"` in devices.json |
278
+ | a full **name** | `"desk lamp"` matches *Desk Lamp* |
279
+ | a **word** of a name | `desk` or `lamp` matches *Desk Lamp* |
280
+ | `bed,setup` | several, comma-separated |
281
+
282
+ 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.
283
+
284
+ Give your bulbs short zone names by adding `"zone"` to their entries in `~/.config/tuya-local-mcp/devices.json`:
285
+
286
+ ```json
287
+ { "name": "Lámpara de noche", "zone": "bed", "id": "...", "key": "...", "ip": "192.168.1.52", "version": "3.3" }
288
+ ```
289
+
290
+ ## Presets and scenes
291
+
292
+ 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`).
293
+
294
+ A **preset** is one setting for whatever you target:
295
+
296
+ ```json
297
+ {
298
+ "chill": { "temp": 0, "brightness": 20, "description": "Dim, warm and relaxed" },
299
+ "cyber": { "rgb": [255, 0, 255], "brightness": 90 }
300
+ }
301
+ ```
302
+
303
+ A **scene** gives each target its own setting:
304
+
305
+ ```json
306
+ {
307
+ "focus": {
308
+ "targets": {
309
+ "ceiling": { "on": false },
310
+ "setup": { "temp": 90, "brightness": 85 },
311
+ "bed": { "temp": 0, "brightness": 15 }
312
+ }
313
+ }
314
+ }
315
+ ```
316
+
317
+ 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.
318
+
319
+ 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.
320
+
321
+ ## Claude Code features
322
+
323
+ ### `/light`
324
+
325
+ ```text
326
+ /light focus
327
+ /light bed 20
328
+ /light fireplace vibe on the desk
329
+ /light apaga todo en media hora
330
+ /light save this as "reading": warm, 60%
331
+ ```
332
+
333
+ 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`.
334
+
335
+ ### Build and test signals
336
+
337
+ | What happens | The lights |
338
+ |---|---|
339
+ | A test or build command Claude runs **fails** | turn red (their previous state is saved) |
340
+ | It **passes** again | flash green for 4 s, then go back to exactly how they were |
341
+ | It passes and nothing had failed | don't change, so green runs don't flicker |
342
+
343
+ 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.
344
+
345
+ ### Ambient mode
346
+
347
+ 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.
348
+
349
+ ### Options (`/config` → tuya-local)
350
+
351
+ | Option | Default | |
352
+ |---|---|---|
353
+ | Build/test signals | on | turn the red/green signals on or off |
354
+ | Signal lights | `all` | which target shows them, e.g. `setup` |
355
+ | Signal commands (regex) | built-in list | your own regular expression for "test or build" commands |
356
+ | Ambient mood | off | let Claude set the mood on its own |
357
+
358
+ Outside Claude Code, see [Claude Desktop](#4b-or-add-it-to-claude-desktop-any-plan-including-free) 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`](plugin/hooks/hooks.json) into your `settings.json`.
359
+
360
+ ### MCP tools
361
+
362
+ | Tool | Does |
363
+ |---|---|
364
+ | `power_light(state, target, delay_seconds)` | on / off / group toggle |
365
+ | `set_light(target, preset \| brightness, temp, rgb, delay_seconds)` | apply a preset or scene, or set values |
366
+ | `light_status(target)` | what each light is doing now, plus the local time |
367
+ | `list_lights()` | lights, zones, the names that target them, IPs, presets |
368
+ | `get_presets()` / `upsert_preset(name, config)` / `delete_preset(name)` | manage presets.json, with validation |
369
+ | `list_timers()` / `cancel_timer(timer_id \| "all")` | see and cancel pending timers |
370
+ | `scan_lan_ips()` | find moved lights and save their new IPs |
371
+
372
+ The server also offers an **`ambient_mode` prompt** that switches ambient mode on for one conversation, in any client that supports MCP prompts.
373
+
374
+ Every result includes a one-line `summary` and per-light `ok`/`error`, so a failure is never reported as success.
375
+
376
+ ## Troubleshooting
377
+
378
+ **A light is "unreachable".** It's probably one of these:
379
+
380
+ 1. **The Smart Life app is open** on a phone and holding the bulb's only local connection. Force-close it.
381
+ 2. **The bulb is switched off at the wall.** Bulbs can't be reached without power.
382
+ 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`.
383
+ 4. **Firewall.** Allow Python to receive UDP 6666/6667 (discovery) and connect to TCP 6668.
384
+
385
+ **"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.
386
+
387
+ **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.
388
+
389
+ **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.
390
+
391
+ ## Security
392
+
393
+ - Bulb keys and Tuya cloud credentials stay in `~/.config/tuya-local-mcp/`, outside any repository. Set `TUYA_LOCAL_MCP_HOME` to use another folder.
394
+ - Files there are written atomically, so a crash can't corrupt them.
395
+ - After the one-time setup, nothing talks to the internet. You can even revoke the Tuya cloud project.
396
+ - This repository's `.gitignore` excludes every file the wizard creates, in case you run it in the repo by mistake.
397
+
398
+ ## Development
399
+
400
+ ```bash
401
+ git clone https://github.com/meow-dot-py/tuya-local-mcp
402
+ cd tuya-local-mcp
403
+ uv sync
404
+ uv run pytest # uses fake bulbs; no hardware or network needed
405
+ uv run ruff check . && uv run ruff format --check .
406
+ ```
407
+
408
+ To use your working copy everywhere while you hack on it: `uv tool install --editable .`
409
+
410
+ To test the plugin from the repo without installing it: `claude --plugin-dir ./plugin`.
411
+
412
+ Each module has one job and a docstring that explains it. Start with [`src/tuya_local_mcp/__init__.py`](src/tuya_local_mcp/__init__.py) for the map.
413
+
414
+ ## License
415
+
416
+ [MIT](LICENSE) © 2026 Johann 'M' Nestorida. Built on [tinytuya](https://github.com/jasonacox/tinytuya) and the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk). Not affiliated with Tuya.