Library / SDK
mihai-dinculescu/tapo avatar
mihai-dinculescu/tapo

tapo: a Rust, Python and MCP client for TP-Link Tapo devices

🦀 Rust API, 🐍 Python API, and 🤖 MCP Server for TP-Link Tapo smart devices

862 stars86 forksRustMIT

At a glance

What is it?
tapo is an unofficial client library for TP-Link's Tapo smart home devices, with a Rust core published to crates.io, a Python package built on top of it through PyO3, and an MCP server that lets an AI assistant call the devices as tools. The device coverage is wide, from L510 bulbs and P110 plugs to hubs, sensors and cameras.
Who is it for?
Adopt tapo if you want to control Tapo hardware from Rust or Python, or to expose it to an AI agent over the Model Context Protocol, because the same core serves all three surfaces and the device list is far wider than the smart plug and bulb most people own.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One core, three ways in

tapo is an unofficial Tapo API client that works with TP-Link Tapo smart devices, and it is published in three forms from one repository.

There is the Rust library, the tapo crate on crates.io with its documentation on docs.rs. There is the Python library, the tapo package on PyPI. And there is an MCP server, in a separate tapo-mcp directory, that exposes Tapo devices as AI-callable tools and resources through the Model Context Protocol, with its own setup and usage documentation.

The repository is a Cargo workspace whose members are tapo, tapo-py and tapo-mcp, and the workspace resolution is set to version 3. The interesting detail is the default-members line, which is set to tapo and tapo-mcp only, with a comment explaining that tapo-py is excluded from bare workspace-level commands such as cargo run --example and cargo test, because it enables pyo3/extension-module, which leaves the Python C API symbols undefined, and feature unification would propagate that into tapo and break the linking of binaries and examples. It has to be built explicitly with -p tapo-py or through maturin.

That is the kind of configuration comment that saves an afternoon, and it is a sign of a project where the Python binding is treated as a real consumer of the core rather than an afterthought.

The licence is MIT, the primary language is Rust, the default branch is main, and the last push was on 2026-10-19. The creator and maintainer is Mihai Dinculescu, and the README says the project was inspired by plugp100 and by python-kasa.

The device list is wide, and it is versioned per model

The README lists what has been tested, and the list is the clearest statement of the project's scope.

The light bulbs are L510, L520, L530, L535, L610 and L630. The light strips are L900, L920 and L930. The plugs are P100, P105, P110, P110M, P115, P125, P125M and TP10, and the power strips are P300, P304M, P306 and P316M. The hubs are H100, H110, H200 and H500. The switches are S200B, S200D and S210. The sensors are KE100, T100, T110, T300, T310 and T315. The cameras are C120, C210, C220, C225, C325WB, C520WS, TC40 and TC70.

What matters about that list is that the client is not uniform across it. A bulb takes a brightness and a colour temperature, a plug takes a boolean, a power strip has per-socket state, a sensor reports measurements, a camera has a stream and a pan and tilt, and a hub has the child devices attached to it. The client therefore has to model a family of devices with different capabilities, and the API shape reflects that: you ask the client for a specific device type and get back a type that only exposes what that device can do.

The README points to a separate SUPPORTED_DEVICES.md file for the supported devices and the feature matrix, which is where the per-model detail lives, and to a TROUBLESHOOTING.md for common problems. The word tested in the device list is doing real work: it distinguishes the models the author has verified from the ones the protocol might also cover.

The Rust API is a builder over a device type

The Rust usage is short enough to read in one screen, and the shape of it is the design.

Adding the dependency is a normal Cargo line:

toml
[dependencies]
tapo = "0.11"

And the client is built by naming the account, the device type and the address:

rust
let device = ApiClient::new("<tapo-username>", "tapo-password")
    .p110("<device ip address>")
    .await?;

device.on().await?;

The pattern is a builder. ApiClient::new takes the credentials, the device type is a method on the client, and the address is a string that is resolved during the await, so the result is a handle to one specific device. The method name is the model, p110 for the P110 plug, and the returned object only exposes what that model can do, which is how the type system keeps a caller from asking a bulb for a power state.

Turning the device on is then a single call, device.on().await?, and the fallible result is the error type the crate uses throughout, since the README's Rust snippet propagates it with the ? operator.

The examples live in the tapo/examples directory and are run the usual Cargo way, with the credentials passed in the environment:

bash
export TAPO_USERNAME=
export TAPO_PASSWORD=
export IP_ADDRESS=

cargo run --example tapo_l530

The example name carries the model, tapo_l530, so the examples are also a catalogue of what each device type supports.

The Python package is the same core through PyO3

The Python library is not a reimplementation. It is the Rust library exposed through PyO3, which is why the workspace configuration treats it specially.

Installing it is the usual pip line:

bash
pip install tapo

And the API is the Rust API with Python syntax, an ApiClient with the credentials, an awaited per-model method, and an awaited call on the device:

python
client = ApiClient("<tapo-username>", "tapo-password")
device = await client.p110("<device ip address>")

await device.on()

The difference from the Rust snippet is the await on the client method, because the model and address resolution is asynchronous here too.

Working on the Python side takes a few more steps, and the README documents them because a PyO3 binding does not build like a normal Python package. You change into the tapo-py directory, create a virtual environment with uv on the first run, sync it with the dev group and a locked resolution, and run maturin develop, which the comment says is needed on the initial run and whenever the Rust code is modified:

bash
cd tapo-py
uv venv # On the initial run only
uv sync --group dev --locked
uv run maturin develop # On the initial run and whenever the Rust code is modified

Then the examples are run with uv run python examples/tapo_p110.py, and they live in the tapo-py/examples directory.

The build tool choice is consistent throughout: uv for the environment and the lockfile, maturin for the extension module, and a locked sync so that the dev environment is reproducible. The workspace comment about feature unification is the reason this is not as simple as a pip install, and the README is upfront about that.

An MCP server for agents, on its own version line

The third surface is the one that is newest, and the version numbers show it.

The MCP server exposes Tapo devices as AI-callable tools and resources through the Model Context Protocol, and the README points to its own README in the tapo-mcp directory for setup and usage. The point of it is straightforward: an assistant that can call a tool can then turn a lamp off, or read a sensor, without the user having to find the right app.

The releases tell you it is young and moving on its own schedule. The Rust library is at v0.11.0 on 2026-10-02 and v0.11.1 on 2026-10-04, while the MCP server is at tapo-mcp-v0.5.3 on 2026-10-04. Two independent version lines, one at 0.11 and one at 0.5, published on the same day, which is a strong hint that they will not always be in step and that a deployment pinning both needs to check the compatibility notes in the changelog rather than assuming the numbers move together.

The design question the MCP server raises is worth naming, because it is the same question every agent-facing tool faces. Turning a physical device into a tool that a model can call means the model can now switch off a heater, and the safety story of that is a policy question rather than a protocol question. The README does not discuss it, and the tools and resources the server exposes are where a careful adopter should look first.

There are also two community projects that show what the library is used for beyond scripting. tapo-rest is a REST wrapper of the library, described as deployable as a service and as an advanced example, and tapoviz does audio-reactive and screen-reactive lighting for the L920 and L930 LED strips, with a local web interface. Both are the kind of project that only becomes possible once the protocol is handled well enough that the interesting work is elsewhere.

The repository shape, and how the project is put together

The top-level listing shows a workspace with a lot of small quality-of-life files, and each one says something.

There is a Makefile.toml, which is the cargo-make task runner, and taplo.toml plus rustfmt.toml for formatting, one for the TOML files and one for the Rust. There is a .cargo/ directory for Cargo configuration and a .claude/ directory. And there is CHANGELOG.md, CONTRIBUTING.md, CLAUDE.md, SUPPORTED_DEVICES.md, TROUBLESHOOTING.md, Cargo.lock and LICENSE.

The workspace dependency list in Cargo.toml is a good summary of what the core is built on. It has anyhow and thiserror for errors, serde and serde_json for serialisation, schemars for JSON schema generation, which matters for an MCP server where tool schemas have to be published, chrono and chrono-tz for time, rand, base64, log, pyo3 at 0.29 for the Python binding, and tokio at 1.52 with default features off, so the runtime features are chosen by the consumer rather than pulled in wholesale. The versions are all current-generation, and the manifest uses the workspace dependency table so that the three members agree on one set of versions.

Two conventions in the repository are worth copying. Every crate is a workspace member, so a change to a shared dependency is a single edit. And the two language surfaces are kept apart by design, with the Python member excluded from the default set for a documented reason, rather than being allowed to break the other builds by accident.

For anyone extending the client, the two documents that matter are SUPPORTED_DEVICES.md, which says what each model can do, and TROUBLESHOOTING.md, which is where the failure modes of talking to a vendor's devices are written down. The README itself stays short and links to both.

Editorial conclusion

Adopt tapo if you want to control Tapo hardware from Rust or Python, or to expose it to an AI agent over the Model Context Protocol, because the same core serves all three surfaces and the device list is far wider than the smart plug and bulb most people own. Do not adopt it as a supported or guaranteed interface, since it is an unofficial client for a vendor protocol that the vendor can change at any time, and treat the network credentials as long-lived secrets because the API takes a username and password rather than a scoped token. Before you build on it, check SUPPORTED_DEVICES.md for the exact model you have, read TROUBLESHOOTING.md before blaming your network, and pin the version, since the Rust and MCP packages move on separate version lines.

Frequently asked questions

How do I install the tapo library?

For Rust, add tapo = "0.11" to the dependencies section of your Cargo.toml. For Python, run pip install tapo. To work on the Python binding itself you need uv and maturin, since the package is a PyO3 extension and is developed with uv venv, uv sync --group dev --locked and uv run maturin develop.

Which Tapo devices does the tapo library support?

The README lists tested bulbs L510, L520, L530, L535, L610 and L630, strips L900, L920 and L930, plugs P100 through TP10 including P110, power strips P300 through P316M, hubs H100, H110, H200 and H500, switches S200B, S200D and S210, sensors KE100 and the T series, and cameras C120 through TC70. The full feature matrix is in SUPPORTED_DEVICES.md.

What does the tapo MCP server do?

It exposes Tapo devices as AI-callable tools and resources through the Model Context Protocol, so an assistant can operate the devices directly. It lives in the tapo-mcp directory with its own setup documentation, and it is released on its own version line, at 0.5.3 while the Rust library was at 0.11.1.

Is the tapo library an official TP-Link API?

No. The README calls it an unofficial Tapo API client, and it is MIT licensed and maintained by Mihai Dinculescu, inspired by plugp100 and python-kasa. The README points to TROUBLESHOOTING.md for common problems and CONTRIBUTING.md for contributions.

Official sources

  1. Issues
  2. License: MIT
  3. mihai-dinculescu/tapo on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mihai-dinculescu-tapo.svg)](https://hysenlabs.com/projects/mihai-dinculescu-tapo)

Was this useful?