tuya-local-mcp
An MCP server that controls Tuya / Smart Life smart home devices over your local network, with no cloud calls after initial setup.
List every device in the registry with names, IDs, IPs, and protocol versions (local keys are never exposed).
Discover devices broadcasting on the LAN by passively listening for beacons (sends nothing).
Read the current status of one device or all devices, including raw datapoints and decoded readings.
Turn devices on or off, optionally specifying the relay datapoint.
Set a light's brightness as a percentage (0–100).
Set a light's color from a hex string such as
#FF8800orf80.Set a light to white mode with a color temperature (0 warm–100 cool), optionally setting brightness too.
Write raw Tuya datapoints for devices not covered by the helpers (fans, curtains, valves, heaters, etc.).
Optionally run in read-only mode so all write tools refuse while reads and discovery still work.
Supports caching, per-device timeouts, custom device registry paths, and configurable discovery timeouts.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@tuya-local-mcp turn on the living room lamp"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
tuya-local-mcp
An MCP server that controls Tuya / Smart Life smart home devices over your local network. Once set up, no request touches Tuya's cloud — commands go straight to the device on your LAN, so they keep working with the internet down and aren't subject to cloud API rate limits.
Built on tinytuya.
The one catch, up front
"Local control" still requires one trip through Tuya's cloud. Each device has a 16-character local_key that's provisioned during pairing and is never broadcast on the network — you can't sniff it, derive it, or guess it. You fetch it once from the Tuya IoT Platform, and after that everything is local, forever.
There is no way around this short of reflashing the hardware with ESPHome or Tasmota. If you'd rather avoid the developer-console setup entirely, use a cloud-based Tuya MCP server instead; this project won't save you that step.
Related MCP server: HueMCP
Install
Requires Python 3.10+.
git clone https://github.com/ethanj2k/tuya-local-mcp
cd tuya-local-mcp
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .Setup
1. Check your devices are visible
Before touching any credentials, confirm the devices are actually reachable. This listens for the UDP beacons Tuya devices broadcast every few seconds and sends nothing:
python scripts/passive_scan.py 153 device(s):
192.168.1.50 udp/6667 beacons=3 proto=3.3 id=bf0000000000000000aaaa
192.168.1.51 udp/6667 beacons=2 proto=3.4 id=bf0000000000000000bbbb
192.168.1.52 udp/6667 beacons=3 proto=3.3 id=bf0000000000000000ccccNo output? See Troubleshooting discovery — on Windows the usual cause is the firewall, not the devices.
2. Get your local keys
Create an account at iot.tuya.com.
Cloud → Development → Create Cloud Project. Pick the data centre matching your region and "Smart Home" as the industry.
In the project, Devices → Link App Account and scan the QR code with the Smart Life / Tuya Smart app. This imports the devices you already own.
Grab one device's Virtual ID from the app: tap the device → pencil icon → Device Information. tinytuya needs one id to bootstrap the list.
Fetch the keys. Either the stock interactive wizard:
python -m tinytuya wizard…or the non-interactive fetcher bundled here, which takes the same inputs from a .env file. Useful in containers, CI, or anywhere a prompt is awkward:
cp .env.example .env # fill in the four values
python scripts/fetch_keys.pyquerying Tuya Cloud (region us)...
cloud returned 6 device(s), 6 with a local key
listening 20s for LAN addresses (transmitting nothing)...
matched 6 device(s) to a LAN address
wrote devices.json (contains local keys - do not commit)
Living Room Lamp 192.168.1.50 v3.3 key=yes
Desk Plug 192.168.1.51 v3.4 key=yesEither way you end up with devices.json. It contains your local keys — never commit it. This repo's .gitignore already excludes it, along with .env.
3. Point the server at the file
The server looks for snapshot.json then devices.json in the current directory, your home directory, and ~/.tinytuya/. Override with TUYA_DEVICES_FILE.
4. Wire it into your MCP client
{
"mcpServers": {
"tuya-local": {
"command": "/absolute/path/to/.venv/bin/python",
"args": ["-m", "tuya_local_mcp"],
"env": {
"TUYA_DEVICES_FILE": "/absolute/path/to/devices.json"
}
}
}
}On Windows the command is C:\\path\\to\\.venv\\Scripts\\python.exe.
Tools
Tool | Writes? | Description |
| no | Every device in the registry — names, ids, IPs, protocol versions. Local keys are never returned. |
| no | Passively listen for devices broadcasting on the LAN. Flags any not in your registry. |
| no | Read one device's datapoints, plus a decoded reading of the common ones. |
| no | Read every device; per-device errors don't fail the whole call. |
| yes | Turn a device on or off. |
| yes | Set a light's brightness, 0–100%. |
| yes | Set a light's colour from hex ( |
| yes | White mode at a colour temperature, 0 (warm) to 100 (cool). |
| yes | Write a raw datapoint — the escape hatch for fans, curtains, valves, heaters. |
Devices are addressed by friendly name, device id, or IP. Name matching is case-insensitive and accepts unique substrings, so "desk" finds "Desk Plug". Ambiguous matches raise an error listing the candidates rather than picking one.
Read-only mode
Set TUYA_READ_ONLY=1 and every write tool refuses. Discovery and status reads still work. Useful when you want an assistant that can answer "is the garage light on?" without being able to act, or while you're still building trust in a setup.
Configuration
Variable | Default | Meaning |
| auto-detected | Path to |
|
|
|
|
| Per-device socket timeout, seconds. |
|
| Seconds to cache status reads. |
|
| Default listen window for |
Troubleshooting discovery
If passive_scan.py hears nothing, work down this list before concluding the devices are offline.
Windows: allow the base interpreter, not the venv one. This is the single easiest thing to lose an hour to, and the obvious fix is the wrong one.
Windows Firewall allows inbound traffic per program path. A venv's Scripts\python.exe on Windows is only a shim — the process that actually binds the socket is the base interpreter it was created from. So a rule naming .venv\Scripts\python.exe matches nothing, and your scan stays empty with no prompt and no error.
Confirm the real image path of whatever interpreter you're running:
.\.venv\Scripts\python.exe -c "import ctypes,sys; b=ctypes.create_unicode_buffer(1024); ctypes.windll.kernel32.GetModuleFileNameW(None,b,1024); print('reported:',sys.executable); print('actual :',b.value)"reported: C:\...\project\.venv\Scripts\python.exe
actual : C:\Users\you\AppData\Local\Programs\Python\Python313\python.exeWrite the rule against the actual path (elevated PowerShell):
New-NetFirewallRule -DisplayName "Tuya LAN discovery" -Direction Inbound `
-Program "C:\Users\you\AppData\Local\Programs\Python\Python313\python.exe" `
-Protocol UDP -LocalPort 6666,6667,7000 -Action AllowBecause the rule lands on the base interpreter, it applies to every venv built from that install — convenient, but worth knowing you're opening those ports for all of them.
To see which interpreters are already allowed:
Get-NetFirewallApplicationFilter | Where-Object { $_.Program -like '*python*' } |
ForEach-Object { $_.Program }Note this only affects discovery. Device control is outbound TCP on port 6668 and works fine without any inbound rule — so a blocked scan does not mean a broken install. If your registry already has current IPs, everything else works.
Devices on another network segment. Beacons are broadcast and do not cross subnets or VLANs. IoT devices parked on a guest network won't be visible from your main one.
Another listener holds the ports. Home Assistant, a second copy of this script, or tinytuya's own scanner will contend for UDP 6666/6667. passive_scan raises if it can't bind anything.
Beacons are periodic. A 5-second window can genuinely miss a device. Try 30 seconds before worrying.
Notes and known limits
Protocol 3.4 / 3.5 devices use a session-key handshake rather than the local key directly. tinytuya supports them, but they're less battle-tested than 3.3 — if exactly one device misbehaves, check its version first.
Local keys rotate whenever a device is removed and re-paired in the app. Symptom is error 914; fix is re-running the wizard.
One connection at a time. Most Tuya devices accept a single TCP connection, so this server opens and closes a socket per call rather than holding one open. If a call times out, check nothing else (Home Assistant, another script) is polling that device continuously.
Datapoint numbering is per-product. The bulb helpers cover the two common layouts (datapoints 1–5 on older bulbs, 20–24 on newer). For anything else, get_status shows the raw datapoints and set_datapoint writes them.
Discovery is best-effort. Beacons are periodic, so a short listen window may miss devices that are simply between broadcasts. Increase the timeout rather than concluding a device is offline. A 20-second scan and a 75-second scan of the same network can differ by several devices.
Zigbee and BLE sub-devices are not locally controllable by this server. Your cloud account will list them, and fetch_keys.py will happily write them into devices.json, but they have no IP of their own — they're reached by routing a command through their gateway using node_id. That routing isn't implemented here. They're identifiable by a node_id or gateway_id field; scripts/inspect_registry.py separates them out for you.
A device switched off at the wall is invisible. It can't broadcast, so it gets no IP and can't be controlled — local control has no cloud fallback to hide this. This is the normal reason a cloud account lists far more devices than the scan finds, and it isn't a fault. Run scripts/inspect_registry.py to see which entries actually have an address.
Development
pip install -e ".[dev]"
pytestThe test suite runs entirely offline — no device, no network, no credentials. It includes a guard test asserting that passive_scan never calls send, sendto, sendall, or connect on a socket.
License
MIT
Available Tools
9 toolsdiscover_devicesA
Passively listen for Tuya devices broadcasting on the local network.
This transmits nothing - it only receives the beacons devices emit every few seconds. Useful for finding a device whose IP has changed, or for spotting devices that are on the network but missing from the registry.
Inputs: timeout. Returns: object
get_all_statusA
Read the state of every device in the registry.
Devices that fail are reported individually rather than failing the call, so one offline device does not hide the rest.
Inputs: no named parameters. Returns: object
get_statusA
Read the current state of a device.
device may be a friendly name, a device id, or an IP address. Returns the
raw Tuya datapoints plus a best-effort interpretation of the common ones.
Inputs: device (required). Returns: object
list_devicesA
List every Tuya device in the local registry.
Returns names, device ids, IPs and protocol versions. Local keys are never
returned. Use the name field when calling the other tools.
Inputs: no named parameters. Returns: object
set_brightnessA
Set a light's brightness as a percentage (0-100).
Inputs: device (required), percent (required). Returns: object
set_colorA
Set a light's colour from a hex string such as '#FF8800' or 'f80'.
Inputs: color (required), device (required). Returns: object
set_color_tempA
Set a light to white mode at a colour temperature percentage (0 warm, 100 cool).
Optionally set brightness in the same call.
Inputs: device (required), percent (required), brightness. Returns: object
set_datapointA
Write a raw Tuya datapoint.
The escape hatch for products whose datapoints the helper tools do not cover - fans, curtains, heaters, valves. Call get_status first to see which datapoints a device exposes and what types they hold.
Inputs: dp (required), value (required), device (required). Returns: object
set_powerA
Turn a device on or off.
switch_dp overrides the datapoint carrying the relay/switch. Leave it
unset to let tinytuya pick, which is correct for the large majority of
plugs, switches and bulbs.
Inputs: on (required), device (required), switch_dp. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 9 tools
Each tool has a clearly distinct purpose: enumeration of registered devices (list_devices), passive network discovery (discover_devices), reading state for one or all devices (get_status/get_all_status), and distinct control actions (power, brightness, color, color temp, raw datapoint). The get_* and set_* families are parallel but not overlapping.
All tool names follow a consistent lowercase snake_case verb_noun pattern: list_devices, discover_devices, get_status, get_all_status, set_power, set_brightness, set_color, set_color_temp, set_datapoint. No mixed conventions or vague verbs.
Nine tools is well-scoped for a Tuya local control server: two device discovery/listing, two status reads, five control operations including a raw escape hatch. Each tool earns its place without redundancy or bloat.
The surface covers the core lifecycle of interacting with local Tuya devices: discovery, listing, status reading, and common control actions. The raw set_datapoint tool fills gaps for unsupported product types. A minor gap is the lack of a tool to add discovered devices to the registry or update device metadata.
Maintenance
Related MCP Connectors
Yeelight device discovery, state, control, and scene execution.
Control a Loxone Miniserver smart home: lights, blinds, climate, scenes and energy.
Control your Philips lights, Sonos speakers, Roborock vacuums and more with Port OS cloud.
Yeelight home metadata and guarded management workflows.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables AI assistants to control Tuya/Smart Life smart home devices via tools like on/off, brightness, color, and custom commands.1055 PyPI1MIT
- AlicenseAqualityDmaintenanceEnables discovery and control of Philips Hue lighting devices via a local bridge using the CLIP v2 API, without any cloud dependency.10MIT
- FlicenseNot gradedqualityDmaintenanceEnables control of Yeelight smart lights over local LAN, supporting on/off, brightness, color, temperature, and auto discovery.-
- AlicenseNot gradedqualityCmaintenanceEnables control of Tuya smart lighting over the local home network, including reading device state, turning devices on or off by name, and switching between white and colour modes with RGB or temperature settings. Devices and credentials stay on the server and are scoped per household principal, so each caller only sees their own home's lights.MIT