Skip to main content
Glama
README.md
<!-- brand:header:start -->
<h1 align="center">WhiteOmadaMcp</h1>

<p align="center">
  <b>Ask an AI assistant about your network in plain language: why the Wi-Fi drops,<br>which port a device is on, whether anything is interfering. It answers from the controller's own data.</b>
</p>
<p align="center">
  An MCP server for TP-Link Omada. Read-only unless you open two separate gates; any change is a dry run first, then read back and diffed.
</p>

<p align="center">
  <img alt="Node.js 18+" src="https://img.shields.io/badge/Node.js-18%2B-5FA04E?logo=nodedotjs&logoColor=white">
  <img alt="MCP stdio" src="https://img.shields.io/badge/MCP-stdio-5A5A5A?logo=modelcontextprotocol&logoColor=white">
  <img alt="TP-Link Omada 6.2" src="https://img.shields.io/badge/TP--Link%20Omada-6.2-4ACBD6?logo=tplink&logoColor=white">
  <br>
  <img alt="Open API v1" src="https://img.shields.io/badge/Open%20API-v1-5A5A5A">
  <img alt="GUI API v2" src="https://img.shields.io/badge/GUI%20API-v2-5A5A5A">
  <img alt="dependencies none" src="https://img.shields.io/badge/dependencies-none-brightgreen">
  <br>
  <img alt="54 offline tests" src="https://img.shields.io/badge/tests-54%20offline-brightgreen">
  <img alt="default read-only" src="https://img.shields.io/badge/default-read--only-brightgreen">
  <img alt="licence MIT" src="https://img.shields.io/badge/licence-MIT-blue">
</p>

<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="docs/assets/whiteomadamcp-architecture-dark.svg">
    <img alt="WhiteOmadaMcp: you ask an AI assistant about your network in plain language; the model picks tools, which this server validates, gates, sends to the Omada controller over the Open API or the GUI API, and verifies; the answer comes back with the evidence from switches, access points, SSIDs, clients and logs." src="docs/assets/whiteomadamcp-architecture-light.svg" width="100%">
  </picture>
</p>

## At a glance

<table>
  <tr><td width="190"><b>What it does</b></td><td>Lets you ask an MCP client such as Claude Desktop or Claude Code questions about your network, like "why does my phone keep dropping Wi-Fi?" or "which switch port is the printer on?". The model picks from 59 tools that read the Omada controller (switches, ports, VLANs, PoE, access-point radios, SSIDs, clients, topology and logs) and answers with the evidence. Diagnostics for roaming, DFS/radar and non-WiFi interference report measurements, not guesses.</td></tr>
  <tr><td width="190"><b>How it works</b></td><td>Speaks both TP-Link's documented Open API (v1) and the undocumented v2 API the web GUI drives. Each operation tries the preferred API, falls back to the other, and reports which one answered, because a controller upgrade can silently move an operation between them.</td></tr>
  <tr><td width="190"><b>Safety</b></td><td>Writes need write credentials AND <code>OMADA_ALLOW_WRITES=true</code>; without both, a write tool returns the exact request it would have sent. Writes default to a dry run, are read back and diffed, secrets are redacted from every output, and no tool can create or extend an account.</td></tr>
  <tr><td width="190"><b>Status</b></td><td>Built and verified against a live Omada software controller 6.2.14.11. The v2 API is undocumented and may differ on other firmware, which is why every write checks itself.</td></tr>
</table>

<table>
  <tr>
    <td align="center"><b>59</b><br>MCP tools</td>
    <td align="center"><b>19</b><br>guarded<br>write tools</td>
    <td align="center"><b>2</b><br>controller APIs<br>with fallback</td>
    <td align="center"><b>54</b><br>offline tests</td>
    <td align="center"><b>0</b><br>runtime<br>dependencies</td>
  </tr>
</table>

## Tech stack

| Area | Used for |
|---|---|
| **Runtime** | Node.js 18+, standard library only; MCP over stdio (JSON-RPC); every tool carries readOnly, destructive and idempotent annotations |
| **Controller access** | Open API v1 (client credentials) and the GUI's v2 API (session); the v2 endpoint map was derived from the controller's own GUI bundles and probed live |
| **Safety** | Separate read and write credential sets, a server-side write gate, dry run by default, read-back diff, recursive secret redaction, SSRF and path-traversal guards |
| **Diagnostics** | Interference from the access points' own radio counters, DFS/radar audit, per-client association history, channel-utilisation swing, client-survival check, security audit |
| **Quality** | 54 offline tests with <code>node:test</code>, 12 read-only live tests that assert a write is refused; long-running RF and DFS collectors for unattended evidence |

---
<!-- brand:header:end -->

## What you can ask

Connect it to an MCP client (Claude Desktop, Claude Code or any other) and ask in plain language. The model chooses the
tools; you get an answer with the evidence behind it.

| You ask | Tools the model uses | What comes back |
| --- | --- | --- |
| "Why does my phone keep dropping Wi-Fi?" | `v2_client_history`, `v2_ssids`, `v2_site_settings` | Every association over N days by AP, channel and band, disconnects per day, and the roaming and SSID settings known to cause drops |
| "Is something interfering with my Wi-Fi?" | `v2_interference_audit`, `v2_channel_util` | Non-WiFi interference measured by the access points themselves, kept apart from ordinary congestion |
| "Did radar push my 5 GHz off its channel?" | `v2_radar_audit`, `v2_dfs_check` | Which radios are on DFS channels, a live watch for channel changes, AP uptime and a log sweep |
| "Which switch port is the TV on, and on which VLAN?" | `v2_past_connections`, `v2_topology`, `v2_switch_ports` | The device, port, VLAN, link speed and PoE state |
| "Is my network configured safely?" | `v2_security_audit`, `v2_health_audit` | Ranked findings, each with the exact fix and its blast radius |
| "Turn off 802.11r on the home SSID." | `v2_set_ssid` | A dry run showing the exact request; with write access, the change applied, read back and verified |

On the reference network, the client-history and SSID checks traced clients dropping while roaming between access points to 802.11r. Turning it
off cut client disconnects from 20.9 to 8.0 an hour.

## Why both APIs

Neither API alone is enough on a modern controller, and each one fails in a way the other covers:

| | Open API (v1) | GUI API (v2) |
| --- | --- | --- |
| Documented and stable | ✅ | ❌ reverse-engineered |
| Event and audit logs | ✅ | ❌ **returns 0 rows on 6.2+** |
| Switch ports, STP, PoE, VLAN profiles | ❌ | ✅ |
| AP radio writes | ❌ silently reverts | ✅ |
| SSID rate control | ✅ | ✅ |

Measured on a 6.2.14.11 controller: the v1 log endpoint returned **88,522 events** over 30 days
where v2 returned **zero**. Meanwhile v1 accepts an AP radio write, reports success, and the
controller quietly keeps the old value.

So every operation declares which transports can serve it, tries the preferred one, falls back,
and **reports which API actually answered**. That last part matters: a controller upgrade that
moves an operation between the two APIs is otherwise invisible, and that exact move is what broke
logs on 6.2.

## Install

```bash
git clone https://github.com/ricardo-david-francisco/WhiteOmadaMcp-public.git
cd WhiteOmadaMcp-public
cp .omada-secrets.env.EXAMPLE .omada-secrets.env
# edit .omada-secrets.env
```

Then point your MCP client at `omada-v2-mcp.js` — see `claude_desktop_config.EXAMPLE.json`.

## Run it read-only, and mean it

The recommended setup is **two instances**:

```
omada        →  read credentials only,  OMADA_ALLOW_WRITES=false
omada-write  →  write credentials,      OMADA_ALLOW_WRITES=true
```

Writes require **both** write credentials and the explicit opt-in. Neither alone is enough, and
the default is read-only, so a fresh clone cannot change your network.

In read-only mode a write tool does not fail silently. It returns the exact request it would have
sent:

```
🔒 REFUSED — this server is READ-ONLY (no write credentials configured).

SSID <your-iot-ssid> 2g rate control -> {"rate2gCtrlEnable":true,"lowerDensity2g":12,...}
transport : v2
current   : {"rate2gCtrlEnable":false,...}

The exact request that was NOT sent:
  PATCH /sites/<siteId>/setting/wlans/<groupId>/ssids/<ssidId>
  { "name": "...", "pskSetting": { "securityKey": "(redacted)" }, ... }
```

`v2_mode` tells you which mode you are in and why.

## Safety rules baked in

1. **Every write is read back and diffed.** A controller that returns `Success` and changes
   nothing is reported as `REVERTED`, not as success. Omada does this on several endpoints.
2. **Writes default to `dryRun: true`.**
3. **Secrets are redacted recursively** before anything is returned. Audit-log diffs embed
   cleartext Wi-Fi passphrases; reading a log should not be a way to harvest every PSK.
4. **Human units at the boundary.** You say `channel: 100` and `widthMHz: 40`; the server handles
   the fact that Omada wants a 1-based index into `channelRange` and `(MHz/20)+1`.
5. **Destructive changes preview their casualties.** Setting an RSSI kick threshold or a minimum
   data rate lists the clients that would be affected *before* sending.
6. **No tool can create, enable or extend an account.**

## What it can tell you

Beyond configuration, the diagnostic tools exist because vague answers are worse than none:

- `v2_radar_audit` — settles a DFS argument with evidence: which radios are actually on DFS
  channels, a live watch for an unrequested channel change, AP uptime, and a per-AP log sweep.
  States plainly what it cannot prove.
- `v2_client_history` — every association a device made over N days, by AP and channel, so you
  can tell a band-steering problem from a roaming problem from a device that simply drops.
- `v2_channel_util` — samples utilisation over time and reports the *swing*, because 2.4 GHz can
  move 30–55 points on its own and a single reading proves nothing.
- `v2_client_survival` — snapshot, wait, re-check, and name anything that disappeared. This is
  how you catch a setting that silently evicted an IoT device instead of just reading "Success".
- `v2_security_audit` — flags PMF **Mandatory** (value `1`) on WPA2-PSK, and WPA2/WPA3 transition
  mode. Omada's PMF enum is `1 = Mandatory, 2 = Capable, 3 = Disabled`; `1` is the strictest value,
  not the weakest. It does **not** tell you to enable 802.11r — on the reference site that was the
  cause of the roaming disconnections, not the cure.

## Errors, decoded

| errorCode | Meaning |
| --- | --- |
| `-1001` | Bad or missing parameters — **the endpoint exists**. Usually a required sibling field. |
| `-1600` | Path does not exist on this build. |
| `-1005` | Licence gate. |
| `-1007` | Your account's role lacks that page. |
| `-1` | Feature needs hardware you do not have. |
| `-30165` | Login refused: 2FA is enabled. The v2 login API cannot complete it. |
| `-33411` | Gateway not connected. |

## Testing

```bash
node --test test/unit.test.js                      # no controller needed
OMADA_LIVE_TEST=1 node --test test/live.test.js    # read-only, against a real controller
```

The live suite forces `OMADA_ALLOW_WRITES=false` and asserts a write is refused, so it cannot
change your network even if the gate were broken.

The development repository's CI runs unit tests on Node 18/20/22/24, CodeQL, gitleaks, an MCP-contract check that every tool
is annotated, and a privacy scan that fails the build on a committed private IP, MAC address or
controller GUID.

## Contributing

Default branch is `main`. Two things will get a PR rejected regardless of merit:

- **A new runtime dependency.** CI enforces zero.
- **A write path that does not read back and diff.** "The controller said Success" is not
  evidence that anything changed.

## Licence

MIT — see [LICENSE](LICENSE).

Not affiliated with TP-Link. The v2 API is undocumented and may change without notice; that is
precisely why every write here verifies itself.

<!-- brand:footer:start -->

---

<p align="center">
  <sub>Maintained by <a href="https://github.com/ricardo-david-francisco">Ricardo David Francisco</a> &nbsp;·&nbsp; MIT licence</sub>
</p>
<!-- brand:footer:end -->

TDQS

B3.1/5.0

Scored across 59 tools

Disambiguation3/5

The set has many distinct tools, but several overlaps: generic v2_get/v2_write/oa_get compete with typed readers/writers; v2_ssid_broadcast and v2_set_ap_ssid both control per-AP SSID state; v2_radar_audit and v2_dfs_check both cover DFS radar evidence; and diagnostic tools like v2_health_audit, v2_diagnostics, v2_rf_health, and v2_security_audit overlap. Descriptions are detailed, which helps, but an agent still faces real misselection risk.

Naming Consistency4/5

Consistent v2_ snake_case prefix and set_* write convention, but read tools are mostly noun-based (v2_clients, v2_ssids, v2_events) rather than the verb_noun pattern, and oa_get/v2_get/v2_write introduce mixed verb styles. Still readable and largely predictable.

Tool Count2/5

59 tools is very heavy; for a single controller domain, many typed tools duplicate what v2_get/v2_write can do, and the diagnostic/audit suite is sprawling. The broad Omada feature set provides some justification, but the surface is over-scoped.

Completeness3/5

Covers extensive read coverage plus many targeted writes, but CRUD is incomplete: no create/delete for VLANs, SSIDs, users (delete only), port profiles, DHCP reservations, or many service objects, and no firmware/backup/reboot actions. Agents can work around some gaps via generic paths, but lifecycle dead ends remain.

Maintenance

ActivityMaintained
ResponsivenessNo issues