The complete third-party runtime tree is one package:
pyserial==3.5
That is not an accident of a small feature set. The MCP protocol layer, the protobuf wire codec and the PNG encoder are all implemented in-tree specifically to avoid pulling a large dependency graph into a program that talks to a USB device and nothing else.
For comparison, pip install mcp (the reference MCP SDK) resolves to 34
packages, including cryptography, PyJWT, uvicorn, starlette,
httpx and python-multipart — a web server stack, its TLS layer and its
authentication layer, none of which this server has any use for. Each is a
package whose maintainer account, CI pipeline and release tooling becomes part
of your attack surface.
What replaced them:
| Concern | Instead of | In-tree |
|---|---|---|
| MCP transport | mcp (+33 transitive) |
jsonrpc.py — newline-delimited JSON-RPC 2.0 on stdio |
| Protobuf | protobuf |
proto.py + pb.py — the messages this server uses, across all three firmware dialects |
| PNG output | Pillow |
png.py — stdlib zlib + struct |
pyserial was kept because cross-platform serial I/O genuinely is fiddly
(termios on POSIX, CreateFile handles on Windows). It is pure Python, has no
build step and no install scripts, and has been stable since 2020.
The npm incidents of mid-2026 are the model this posture is built against:
@redhat-cloud-services/ Miasma (disclosed 1 June 2026) — 96 versions across 32 packages carried a credential stealer in apreinstallscript, which runs automatically during install, before any application code, with no warning. The initial compromise was a maintainer's GitHub account, taken via a malicious VS Code extension.- The
node-gyp/binding.gypworm — self-propagating, and reached packages through native build steps.
Neither of these could reach a Python project, and that is the first line of defence here. But the mechanism generalises: install-time code execution in a transitive dependency you never chose. The mitigations that do generalise:
- A dependency you do not have cannot be compromised. One package instead of thirty-four is the single largest reduction available.
- No install scripts anywhere in the tree.
pyserialis a pure-Python wheel; there is nosetup.pybuild step, no native compilation, no post-install hook. - Hash pinning.
requirements.lockpins by SHA-256 for both the wheel and the sdist. Install withpip install --require-hashes -r requirements.lockand a substituted artifact fails closed, even if PyPI itself is serving it. - Vendored schemas are data, never code. The
proto/{official,momentum,unleashed}/files are reference material for humans readingpb.py. Nothing reads, compiles or executes them at runtime — there is no code generation step to poison, and no build-time fetch from three separate upstream repositories.
- The server makes no network connections. It opens a serial port and
spawns
qFlipper-clias a subprocess. There is no telemetry, no update check, and no outbound socket of any kind. - stdout carries only MCP protocol messages. Every diagnostic goes to stderr; a stray write to stdout would corrupt the transport.
- Subprocess invocation passes an argument list, never a shell string, so there is no shell to inject into.
Destructive operations are hidden, not merely refused. A tool omitted from
tools/list cannot be invoked by a model that never sees it — a stronger
guarantee than asking it to supply a confirmation flag it can satisfy itself.
| Variable | Default | Effect |
|---|---|---|
FLIPPERTALK_TOOLSETS |
all but network |
Which tool groups are exposed at all |
FLIPPERTALK_ALLOW_DESTRUCTIVE |
unset | Exposes qflipper_restore, qflipper_flash_firmware, qflipper_erase, qflipper_wipe, flipper_install_update, recursive delete, and DFU/UPDATE reboot |
FLIPPERTALK_READ_ONLY |
unset | Withholds every mutating tool; only inspection and screenshots remain |
FLIPPERTALK_LOCAL_ROOTS |
unset | Confines host filesystem reads and writes to an allowlist of directories |
FLIPPERTALK_DIALECT |
auto | Forces a firmware family when detection is wrong |
FLIPPERTALK_IDLE_TIMEOUT |
120 |
Seconds before an idle serial connection is dropped |
The QFLIPPER_MCP_* names used before the rename are still honoured for one
version. Dropping them immediately would have silently removed a user's safety
gating on upgrade, which is worse than carrying an alias.
Unleashed firmware exposes TCP, HTTP and WebSocket calls plus GPS position over
RPC. Those are implemented, but the network toolset is not enabled by
default: letting a model open sockets from the device, or read its position, is
a materially different trust decision from reading the SD card. Enable it
deliberately with FLIPPERTALK_TOOLSETS=all, or by naming network explicitly.
flipper_scan_nearby reports any source it could not run as not attempted,
with a reason — never as "nothing found". No Flipper firmware can scan for BLE,
and the device has no WiFi radio. A tool that quietly returned an empty result
for those would lead a model to tell a user their surroundings are clear when
nothing ever looked. The test suite asserts this directly, including that the
summary text never describes an unrun source as clear.
Additional validation:
- Device paths must resolve under
/ext,/intor/any...segments are collapsed before the root is checked, so/ext/../../etc/passwdis rejected rather than silently escaping. - Uploads are verified by md5 against the device after writing, and the tool fails loudly on mismatch rather than reporting success.
The test suite asserts the negative case for each gate — that a hidden tool
is absent from tools/list and refused when called directly. A guard that has
only ever been tested against traffic it should allow is not a guard.
This server gives an MCP client real control of attached hardware: reading and
writing the SD card, launching apps, sending button presses, and (when enabled)
flashing firmware. Treat granting it to a model as equivalent to handing over
the device. Run with FLIPPERTALK_READ_ONLY=1 if you only need inspection.
Open a security advisory on the repository rather than a public issue.