@owine/unifi-protect-mcp 2.14.2 → 3.0.0

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 (60) hide show
  1. package/README.md +248 -92
  2. package/dist/client.d.ts +43 -3
  3. package/dist/client.js +255 -85
  4. package/dist/config.d.ts +7 -1
  5. package/dist/config.js +114 -6
  6. package/dist/index.js +14 -6
  7. package/dist/instructions.d.ts +6 -0
  8. package/dist/instructions.js +24 -0
  9. package/dist/schemas/cameras.d.ts +32 -30
  10. package/dist/schemas/cameras.js +7 -6
  11. package/dist/schemas/common.d.ts +31 -6
  12. package/dist/schemas/common.js +45 -6
  13. package/dist/schemas/devices.d.ts +2 -10
  14. package/dist/schemas/devices.js +35 -26
  15. package/dist/schemas/misc.d.ts +50 -18
  16. package/dist/schemas/misc.js +23 -15
  17. package/dist/schemas/pos.d.ts +1 -1
  18. package/dist/schemas/pos.js +1 -1
  19. package/dist/tls.d.ts +41 -0
  20. package/dist/tls.js +75 -0
  21. package/dist/tools/alarm-manager.d.ts +3 -0
  22. package/dist/tools/alarm-manager.js +32 -0
  23. package/dist/tools/arm-profiles.d.ts +8 -2
  24. package/dist/tools/arm-profiles.js +98 -95
  25. package/dist/tools/cameras.d.ts +2 -2
  26. package/dist/tools/cameras.js +156 -181
  27. package/dist/tools/device-actions.d.ts +2 -2
  28. package/dist/tools/device-actions.js +84 -97
  29. package/dist/tools/devices.d.ts +6 -3
  30. package/dist/tools/devices.js +50 -51
  31. package/dist/tools/files.d.ts +2 -2
  32. package/dist/tools/files.js +82 -57
  33. package/dist/tools/index.d.ts +2 -2
  34. package/dist/tools/index.js +15 -10
  35. package/dist/tools/liveviews.d.ts +2 -2
  36. package/dist/tools/liveviews.js +52 -52
  37. package/dist/tools/pos.d.ts +6 -4
  38. package/dist/tools/pos.js +15 -20
  39. package/dist/tools/registrar.d.ts +34 -0
  40. package/dist/tools/registrar.js +103 -0
  41. package/dist/tools/subscriptions.d.ts +13 -2
  42. package/dist/tools/subscriptions.js +148 -27
  43. package/dist/tools/system.d.ts +2 -2
  44. package/dist/tools/system.js +5 -15
  45. package/dist/tools/users.d.ts +2 -2
  46. package/dist/tools/users.js +18 -35
  47. package/dist/utils/limits.d.ts +33 -0
  48. package/dist/utils/limits.js +74 -0
  49. package/dist/utils/paging.d.ts +15 -0
  50. package/dist/utils/paging.js +28 -0
  51. package/dist/utils/params.d.ts +13 -0
  52. package/dist/utils/params.js +40 -0
  53. package/dist/utils/responses.js +2 -3
  54. package/dist/utils/safety.d.ts +39 -3
  55. package/dist/utils/safety.js +45 -3
  56. package/dist/utils/url.d.ts +4 -1
  57. package/dist/utils/url.js +4 -1
  58. package/dist/version.d.ts +6 -0
  59. package/dist/version.js +11 -0
  60. package/package.json +14 -12
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # UniFi Protect MCP Server
2
2
 
3
- An MCP (Model Context Protocol) server that exposes UniFi Protect's Integration REST API as tools for Claude Code and other MCP clients. Aligned with UniFi Protect API **7.3.47** — 74 tools covering cameras, lights, sensors, chimes, viewers, sirens, fobs, relays, speakers, bridges, link stations, alarm hubs, arm profiles, live views, files, users, point-of-sale events, NVR status, and WebSocket subscriptions.
3
+ An MCP (Model Context Protocol) server that exposes UniFi Protect's Integration REST API as tools for Claude Code and other MCP clients. Aligned with UniFi Protect API **7.3.70**, with tools covering cameras, lights, sensors, chimes, viewers, sirens, fobs, relays, speakers, bridges, link stations, alarm hubs, arm profiles, live views, files, users, point-of-sale events, NVR status, and WebSocket subscriptions.
4
4
 
5
5
  ## Prerequisites
6
6
 
7
- - Node.js 22.x (22.13 or newer) or 24.x — see `engines` in `package.json`
7
+ - Node.js 22.x (22.19 or newer) or 24.x — see `engines` in `package.json`
8
8
  - A UniFi Protect system with the Integration API enabled
9
- - An API key generated from your UniFi Protect console
9
+ - An API key generated from your UniFi Protect console (see [Choosing an API key](#choosing-an-api-key))
10
10
 
11
11
  ## Setup
12
12
 
@@ -15,11 +15,15 @@ An MCP (Model Context Protocol) server that exposes UniFi Protect's Integration
15
15
  Add to Claude Code with a single command — no clone or build needed:
16
16
 
17
17
  ```bash
18
- claude mcp add-json unifi-protect '{"command":"npx","args":["-y","@owine/unifi-protect-mcp@latest"],"env":{"UNIFI_PROTECT_HOST":"192.168.1.1","UNIFI_PROTECT_API_KEY":"your-api-key","UNIFI_PROTECT_VERIFY_SSL":"false"}}' -s user
18
+ claude mcp add-json unifi-protect '{"command":"npx","args":["-y","@owine/unifi-protect-mcp@latest"],"env":{"UNIFI_PROTECT_HOST":"192.168.1.1","UNIFI_PROTECT_API_KEY":"your-api-key"}}' -s user
19
19
  ```
20
20
 
21
21
  Use `-s user` for global availability across all projects, or `-s project` for the current project only.
22
22
 
23
+ A local console usually has a self-signed certificate, so the first call fails with a certificate error until you trust it: see [Self-signed console certificates](#self-signed-console-certificates).
24
+
25
+ `@latest` runs the newest release every time the server starts, including a new major version. To take fixes but choose when to make breaking upgrades, pin the major version you have tested instead (`@owine/unifi-protect-mcp@<major>`).
26
+
23
27
  ### From source
24
28
 
25
29
  If you prefer to build locally:
@@ -37,22 +41,42 @@ This project uses pnpm — `npm install` ignores `pnpm-lock.yaml` and may resolv
37
41
  Then add to Claude Code:
38
42
 
39
43
  ```bash
40
- claude mcp add-json unifi-protect '{"command":"node","args":["/path/to/unifi-protect-mcp/dist/index.js"],"env":{"UNIFI_PROTECT_HOST":"192.168.1.1","UNIFI_PROTECT_API_KEY":"your-api-key","UNIFI_PROTECT_VERIFY_SSL":"false"}}' -s user
44
+ claude mcp add-json unifi-protect '{"command":"node","args":["/path/to/unifi-protect-mcp/dist/index.js"],"env":{"UNIFI_PROTECT_HOST":"192.168.1.1","UNIFI_PROTECT_API_KEY":"your-api-key"}}' -s user
41
45
  ```
42
46
 
43
47
  ### Environment Variables
44
48
 
45
49
  | Variable | Required | Default | Description |
46
50
  |---|---|---|---|
47
- | `UNIFI_PROTECT_HOST` | Yes | — | IP or hostname of your UniFi Protect console |
48
- | `UNIFI_PROTECT_API_KEY` | Yes | — | API key from Protect integration settings |
49
- | `UNIFI_PROTECT_VERIFY_SSL` | No | `true` | Set to `false` to skip TLS certificate verification (needed for self-signed certs) |
50
- | `UNIFI_PROTECT_READ_ONLY` | No | `true` | Set to `false` to enable write/mutating tools (default is monitoring-only mode) |
51
+ | `UNIFI_PROTECT_HOST` | Yes | — | IP or hostname of your UniFi Protect console, optionally with `:port`. A leading `https://` and trailing `/` are stripped; bracket IPv6 addresses |
52
+ | `UNIFI_PROTECT_API_KEY` | Yes | — | API key from the console's integration settings (see [Choosing an API key](#choosing-an-api-key)). Surrounding whitespace is trimmed; a key with a control character inside is refused at startup |
53
+ | `UNIFI_PROTECT_CA_CERT` | No | — | Absolute path to a PEM file with the console's certificate, to verify a self-signed console without turning verification off (see below). Not allowed with `api.ui.com` or together with `UNIFI_PROTECT_VERIFY_SSL=false` |
54
+ | `UNIFI_PROTECT_VERIFY_SSL` | No | `true` | Set to `false` (or `0`/`no`/`off`, any case) to skip certificate verification for this server's connections to the console. Prefer `UNIFI_PROTECT_CA_CERT`. Not allowed with `api.ui.com`. Unrecognized values keep the default |
55
+ | `UNIFI_PROTECT_READ_ONLY` | No | `true` | Set to `false` (or `0`/`no`/`off`, any case) to enable write tools (default is monitoring-only mode). Unrecognized values keep the default |
51
56
  | `UNIFI_PROTECT_CONSOLE_ID` | No | — | Cloud Connector console ID. Only used when `UNIFI_PROTECT_HOST` is `api.ui.com`; ignored (with a warning on stderr) otherwise |
52
57
 
58
+ The startup line in the MCP host's server log says whether the server is read-only or read-write. A configuration error names the variable at fault, and the server exits.
59
+
60
+ ### Self-signed console certificates
61
+
62
+ UniFi consoles ship with a self-signed certificate, so the first call to a local console usually fails with a certificate error. The safe fix is to trust that one certificate. Save it:
63
+
64
+ ```bash
65
+ openssl s_client -connect 192.168.1.1:443 </dev/null 2>/dev/null | openssl x509 > ~/.config/unifi-protect-console.pem
66
+ ```
67
+
68
+ Set `UNIFI_PROTECT_CA_CERT` to the file's **absolute** path (for example `/home/you/.config/unifi-protect-console.pem`); MCP hosts do not expand `~` or `$HOME` in `env`. The server then accepts exactly that certificate, even though it is issued to a name like `unifi.local` and you connect by IP. This also works for a certificate issued by a CA (one you installed on the console yourself): pinning the console's own certificate is enough, without the CA's. Any other certificate must chain to the file **and** match the hostname. If the network between you and the console is not trusted, compare the fingerprint (`openssl x509 -in <file> -noout -fingerprint -sha256`) with the console's. If the console later generates a new certificate, save it again.
69
+
70
+ Setting `UNIFI_PROTECT_VERIFY_SSL=false` instead turns certificate checks off for this server's connections to the console, and only those (other TLS connections in the process are unaffected). Anyone who can intercept that traffic can then read the API key, so use it only on a network you trust.
71
+
72
+ ### Choosing an API key
73
+
74
+ - **Use the least-privileged key that works.** Create the key from an admin account with no more access than you need. For the default read-only mode, that means an admin with view-only access to Protect; use a full-access admin only with `UNIFI_PROTECT_READ_ONLY=false`.
75
+ - **Prefer a local console key to a Site Manager key.** A Site Manager key (Cloud Connector, `api.ui.com`) reaches every console on the account and travels over the internet.
76
+
53
77
  ### Manual Configuration
54
78
 
55
- Alternatively, add to your `~/.claude.json` under the top-level `"mcpServers"` key:
79
+ Alternatively, add to your `~/.claude.json` under the top-level `"mcpServers"` key, or to Claude Desktop's `claude_desktop_config.json` the same way:
56
80
 
57
81
  ```json
58
82
  {
@@ -63,18 +87,13 @@ Alternatively, add to your `~/.claude.json` under the top-level `"mcpServers"` k
63
87
  "env": {
64
88
  "UNIFI_PROTECT_HOST": "192.168.1.1",
65
89
  "UNIFI_PROTECT_API_KEY": "your-api-key",
66
- "UNIFI_PROTECT_VERIFY_SSL": "false"
90
+ "UNIFI_PROTECT_CA_CERT": "/absolute/path/to/console.pem"
67
91
  }
68
92
  }
69
93
  }
70
94
  }
71
95
  ```
72
96
 
73
- ## Safety Features
74
-
75
- This server provides layered safety controls for responsible operation:
76
-
77
- - **Tool annotations** — Every tool declares `readOnlyHint` and `destructiveHint` so MCP clients (like Claude Code) can make informed confirmation decisions
78
97
  ### Cloud Connector
79
98
 
80
99
  To reach a console through Ubiquiti's Cloud Connector instead of over the local
@@ -82,7 +101,12 @@ network, set `UNIFI_PROTECT_HOST=api.ui.com` and `UNIFI_PROTECT_CONSOLE_ID` to
82
101
  the console's ID, and supply a **Site Manager** API key (unifi.ui.com →
83
102
  Settings → API) as `UNIFI_PROTECT_API_KEY`. That is a different credential
84
103
  from a local console API key. Requests are routed through
85
- `/v1/connector/consoles/{id}`. Keep TLS verification enabled.
104
+ `/v1/connector/consoles/{id}`.
105
+
106
+ TLS verification stays on: the server refuses to start with
107
+ `UNIFI_PROTECT_VERIFY_SSL=false` or `UNIFI_PROTECT_CA_CERT` for `api.ui.com`.
108
+ Ubiquiti's cloud has a publicly trusted certificate, and a pinned certificate
109
+ would replace the public certificate authorities for this server's connections.
86
110
 
87
111
  Find your console ID with:
88
112
 
@@ -103,162 +127,292 @@ running Protect will answer.
103
127
  Setting `UNIFI_PROTECT_CONSOLE_ID` against a local host is not fatal: the
104
128
  server logs a warning and talks to that host directly.
105
129
 
106
- - **Read-only mode** — Enabled by default. Only read operations (list, get, snapshot) are registered. Set `UNIFI_PROTECT_READ_ONLY=false` to enable write/mutating tools
107
- - **Confirmation parameter** — The most dangerous tools (`protect_disable_mic`, `protect_trigger_alarm_webhook`) require an explicit `confirm: true` parameter that must be present for the call to succeed
108
- - **Dry-run support** — All write tools (except those with `confirm`) accept an optional `dryRun: true` parameter that returns a preview of what would happen without making any changes
130
+ ## Upgrading to 3.0
131
+
132
+ 3.0 puts more actions behind confirmation: physical outputs, disarming, anything that weakens alarm coverage, and live video and two-way audio. It also scopes TLS settings to this server, keeps every result within what one MCP message can carry, and makes cancellation stop the request to Protect.
133
+
134
+ **If you use the server only through an LLM client** (Claude Code, Claude Desktop, …), the model reads the new parameters from the tool schemas, so most setups need no change. Check three things:
135
+
136
+ - **Cloud Connector with verification off no longer starts.** Unset `UNIFI_PROTECT_VERIFY_SSL` when `UNIFI_PROTECT_HOST=api.ui.com`.
137
+ - **Spellings of `UNIFI_PROTECT_READ_ONLY` / `UNIFI_PROTECT_VERIFY_SSL`** that 2.x ignored now take effect (table below).
138
+ - **Expect more approval prompts.** Update tools and every confirm-gated tool are now annotated destructive (below).
139
+
140
+ Read on if you call tools from scripts or saved agent configurations, or keep allow-lists keyed on tool annotations.
141
+
142
+ ### Check your environment variables first
143
+
144
+ 2.x treated only the exact string `false` as false. 3.0 also accepts `0`, `no` and `off`, in any case, so a value that 2.x silently ignored now takes effect:
145
+
146
+ | Setting | 2.x | 3.0 |
147
+ |---|---|---|
148
+ | `UNIFI_PROTECT_READ_ONLY=0` (or `no`, `off`, `FALSE`) | Read-only | ⚠️ **Write tools enabled** |
149
+ | `UNIFI_PROTECT_VERIFY_SSL=0` (or `no`, `off`, `FALSE`) | Certificate verified | Verification **skipped** |
150
+ | `UNIFI_PROTECT_HOST=api.ui.com` with `UNIFI_PROTECT_VERIFY_SSL=false` (or `0`/`no`/`off`) | Verification skipped | ⚠️ **Startup fails**: "TLS verification cannot be disabled for api.ui.com". Unset `UNIFI_PROTECT_VERIFY_SSL` |
151
+ | `UNIFI_PROTECT_VERIFY_SSL=false` | Turned verification off for **every** TLS connection in the process | Applies only to this server's connections to the console. Prefer the new `UNIFI_PROTECT_CA_CERT` |
152
+ | `UNIFI_PROTECT_API_KEY` pasted with a trailing newline or spaces | Sent as is; every call failed and the error quoted the key | Trimmed |
153
+ | `UNIFI_PROTECT_API_KEY` with a control character, or any character that is not printable ASCII (a zero-width space, a smart quote), inside | Same | Startup fails with a message that does not repeat the key |
154
+
155
+ Any other unrecognized value keeps the safe default (read-only, verification on), as before. New: `UNIFI_PROTECT_CA_CERT` verifies a console's self-signed certificate (see [Self-signed console certificates](#self-signed-console-certificates)). Configuration errors now name the variable at fault, and the startup log line says whether the server is read-only or read-write.
156
+
157
+ ### Tool input changes (breaking)
158
+
159
+ | Tool | 2.x | 3.0 |
160
+ |---|---|---|
161
+ | `protect_disable_arm_alarm` | No confirmation | Requires `confirm: true` |
162
+ | `protect_activate_relay_output` | Omitting `state` toggled the output | Requires `confirm: true` **and** `state` (`"on"` / `"off"`) |
163
+ | `protect_trigger_alarm_hub_output` | Omitting `enable` toggled the output | Requires `confirm: true` **and** `enable` (`true` / `false`) |
164
+ | `protect_create_rtsp_stream`, `protect_create_talkback` | No confirmation | Requires `confirm: true`; the returned URLs are credentials |
165
+ | `protect_stop_siren` | No confirmation | Requires `confirm: true` |
166
+ | `protect_delete_arm_profile`, `protect_set_current_arm_profile` | No confirmation | Requires `confirm: true` |
167
+ | `protect_update_sensor` | No confirmation | Requires `confirm: true` when `settings` include `alarmSettings`, `motionSettings`, `glassBreakSettings`, `armProfileIds` or `scheduleMode` |
168
+ | `protect_update_fob` | No confirmation | Requires `confirm: true` when `settings` include `armControlSettings` |
169
+ | `protect_update_arm_profile` | No confirmation | Requires `confirm: true` when `settings` include `schedules`, `automations`, `activationDelay` or `recordEverything` |
170
+ | `protect_update_siren` | No confirmation | Requires `confirm: true` when `settings` include `volume` |
171
+ | Every tool that takes an ID | Any string | Only IDs as list and get tools return them: letters, digits, `-` and `_`. An arm profile ID cannot be `settings`, `enable` or `disable`, in any case; an alarm webhook ID cannot be `.` or `..` |
109
172
 
110
- ## Tools (73 total)
173
+ A call that leaves out `state` or `enable`, or passes a malformed ID, fails input validation. A call without `confirm: true` returns an error result ("This action requires confirmation: …"), so the agent can ask you and call again.
111
174
 
112
- ### System (2)
175
+ Toggling was removed because it is not safe to retry: a repeated toggle flips a gate or lock back to where it started. To act on the current state, read it first: each entry in `outputs` from `protect_get_relay` carries a `state`, and `protect_get_alarm_hub` reports `alarmHub.output` keyed by channel.
176
+
177
+ ```jsonc
178
+ // 2.x: toggles output 0
179
+ { "id": "<relay id>", "outputId": 0 }
180
+ // 3.0: explicit, so a repeat leaves the output as asked (a pulseDuration pulses again)
181
+ { "id": "<relay id>", "outputId": 0, "state": "on", "confirm": true }
182
+ ```
183
+
184
+ **Relaxed:** a `dryRun: true` preview never needs `confirm`. In 2.x, `protect_disable_mic` and `protect_trigger_alarm_webhook` rejected a dry run without `confirm: true`. Calls that send both still work.
185
+
186
+ **New:** every list tool except `protect_list_nvrs` takes optional `limit` and `offset`. A call with neither returns the whole list, as before.
187
+
188
+ ### Annotation changes (breaking for allow-lists)
189
+
190
+ - `protect_update_camera`, every `protect_update_<device>` tool, `protect_update_arm_profile` and `protect_update_liveview` are now `destructiveHint: true`, and so is every tool that takes `confirm` (`protect_stop_siren`, `protect_set_current_arm_profile`, `protect_create_rtsp_stream`, `protect_create_talkback`, `protect_disable_arm_alarm`, `protect_activate_relay_output`, `protect_trigger_alarm_hub_output` among them). MCP clients that auto-approve non-destructive tools will now ask before running them, and allow-lists keyed on the annotation need updating.
191
+ - Every tool now has a `title` and `openWorldHint: false`.
192
+
193
+ ### Stricter validation
194
+
195
+ - **`UNIFI_PROTECT_HOST`** is parsed as a URL authority. A pasted `https://192.168.1.1/` now works, and `:443` is dropped. A path, credentials, a non-numeric or out-of-range port, or an unbracketed IPv6 address now fails at startup instead of at the first request. Write IPv6 as `[2001:db8::1]` or `[2001:db8::1]:7443`.
196
+ - **`protect_upload_file`** rejects `base64Data` that is not valid base64 (bad characters, length or padding), and empty input, where 2.x uploaded corrupt bytes without an error. A `data:` URL prefix and line breaks are accepted. A file too large to fit one MCP message is refused with "File too large to send in one MCP message"; in 2.x an oversized request closed the server's stdio transport and the server exited with nothing logged.
197
+ - **`protect_update_liveview`** rejects a body whose `settings.id` differs from `id`.
198
+
199
+ ### Output changes
200
+
201
+ - Text results are compact JSON, not indented. `structuredContent` is unchanged; use it if you parse results.
202
+ - Single-object results and dry-run previews now pass MCP clients' output validation. In 2.x, a top-level field the schema did not list, or any dry-run preview of a tool with an output schema, failed with `-32602`.
203
+ - A snapshot, response or other result too large for one MCP message is refused with an error saying so. In 2.x it was sent, and the MCP client dropped the connection.
204
+ - The `protect_upload_file` dry-run preview reports `byteLength` (decoded size) in place of `dataLength` (base64 string length).
205
+ - `protect_subscribe_devices` / `protect_subscribe_events` stop at a message cap **or** a size budget, whichever comes first (both stated in each tool's description), and add `truncated: true`. `duration` is the time actually spent listening. A listen that fails before any message arrives (refused or unauthorized socket) returns an **error result**; in 2.x it returned `{messages: [], error}` as a success. One that fails partway still returns what it collected, with `error`.
206
+ - A timeout names the call and what to do: `PATCH /cameras/…: Protect did not respond within … s`, followed by "Retry when the console responds" for reads, or "The change may still have been applied; check before retrying" for writes. Snapshots and uploads time out before the MCP client's own request timeout, so you see this message instead of a bare client timeout.
207
+ - Network and TLS errors name the underlying cause, e.g. `fetch failed: connect ECONNREFUSED …`, instead of a bare `fetch failed`.
208
+ - A subscription that the console refuses, closes or drops before any message arrives returns an error naming why, instead of an empty list that reads as a quiet site. So does one whose window ends before the connection is established, or whose first message is too large to return.
209
+ - A list tool reads a response of up to several times one MCP message and returns the page asked for, so `limit` and `offset` work on a site whose full list would not fit.
210
+ - Cancelling a tool call aborts its request to Protect, and a cancelled subscription closes its socket. In 2.x a cancelled relay switch could still land afterwards.
211
+ - The server reports its real package version to MCP clients; 2.x always reported `1.0.0`. It also sends server instructions (see [Safety Features](#safety-features)).
212
+
213
+ ### For packagers
214
+
215
+ `undici` is a new runtime dependency (scoped TLS and connection keep-alive). Requests go through undici's own `fetch` rather than Node's, so:
216
+
217
+ - They speak HTTP/1.1, as before, and send `User-Agent: unifi-protect-mcp/<version>`.
218
+ - An HTTP proxy is used as Node's built-in `fetch` would: from `HTTPS_PROXY` / `HTTP_PROXY`, minus `NO_PROXY`, only when `NODE_USE_ENV_PROXY=1` or `--use-env-proxy` is set. A dispatcher installed with undici's `setGlobalDispatcher` no longer applies. The subscription WebSocket never used a proxy, and still does not.
219
+
220
+ ## Safety Features
221
+
222
+ - **Tool annotations.** Every tool declares `readOnlyHint`, `destructiveHint` and `openWorldHint: false` (each talks only to your console), plus a readable `title`, so MCP clients such as Claude Code can decide when to ask you. `destructiveHint: true` marks every tool that deletes or replaces existing state, which includes every `protect_update_*` tool, and every tool that takes `confirm`. Clients that auto-approve non-destructive tools ask before those. The tool tables below mark them **DESTRUCTIVE**.
223
+ - **Server instructions.** On connect, the server gives the MCP host a short guide for the model, matched to read-only or read-write mode. It says to treat names and event text from the console as data, not instructions; that RTSPS and talkback URLs are credentials; and, when writes are enabled, to preview with `dryRun` and ask you before passing `confirm`.
224
+ - **Read-only mode.** On by default. Only the read tools (list, get, snapshot, subscriptions) are registered; write tools do not exist for the model, and registering one in read-only mode fails at startup. Set `UNIFI_PROTECT_READ_ONLY=false` to enable writes. Read-only mode still returns RTSPS URLs from `protect_get_rtsp_streams`, and those are credentials for live video.
225
+ - **Confirmation parameter.** Tools that are irreversible, switch physical outputs, trigger the alarm webhook, stop a siren, disarm or weaken alarm coverage, or open live video or two-way audio refuse to act unless the call carries `confirm: true`. Without it the tool returns an error naming the action, so the agent can ask you first. `protect_update_sensor`, `protect_update_fob`, `protect_update_siren` and `protect_update_arm_profile` need it only when the change touches alarm-related settings (each tool's description names them); renames and LED changes do not. `confirm` is filled in by the model, so it is a prompt for the model to ask you, not a lock; for a human gate, keep your MCP client's approval prompts on for destructive tools.
226
+ - **Dry-run support.** Every write tool accepts `dryRun: true` and returns the request it would send (`dryRun`, `action`, `path`, `body`) without changing anything. A dry run never needs `confirm`.
227
+ - **Request safety.** IDs must be IDs returned by list and get tools, and a request path that would resolve outside the Integration API is refused before it is sent. Redirects are refused, so the API key is never forwarded. Cancelling a tool call aborts its request to Protect.
228
+
229
+ See [SECURITY.md](SECURITY.md) for reporting vulnerabilities and operating the server safely.
230
+
231
+ ## Structured output
232
+
233
+ Read tools, and write tools that return the resource, declare an `outputSchema`, and return `structuredContent` alongside compact JSON text. Schemas are published as loose objects, so a field a firmware update adds flows through instead of failing validation. A list's items are under `result`; a paged list also has `totalCount` and `offset`. `protect_get_snapshot` returns an image. A dry run returns `{dryRun, action, path, body}`. Action endpoints that return no body succeed with an empty result.
234
+
235
+ ## Tools
236
+
237
+ **DESTRUCTIVE** marks tools annotated `destructiveHint: true`. *Requires `confirm: true`* marks tools that take `confirm`; every one of them is also DESTRUCTIVE. *Pages with `limit`/`offset`* marks list tools that take them.
238
+
239
+ ### System
113
240
  | Tool | Description |
114
241
  |---|---|
115
242
  | `protect_get_info` | Get system information and version details |
116
- | `protect_list_nvrs` | List all NVR devices |
243
+ | `protect_list_nvrs` | Get the NVR console info (a single object) |
117
244
 
118
- ### Subscriptions (2)
245
+ ### Subscriptions
119
246
  | Tool | Description |
120
247
  |---|---|
121
- | `protect_subscribe_devices` | Subscribe via WebSocket to device state updates |
122
- | `protect_subscribe_events` | Subscribe via WebSocket to event notifications |
248
+ | `protect_subscribe_devices` | Subscribe via WebSocket to device state updates for a short window (not over Cloud Connector) |
249
+ | `protect_subscribe_events` | Subscribe via WebSocket to event notifications for a short window (not over Cloud Connector) |
123
250
 
124
- ### Cameras (12)
251
+ ### Cameras
125
252
  | Tool | Description |
126
253
  |---|---|
127
- | `protect_list_cameras` | List all cameras |
254
+ | `protect_list_cameras` | List all cameras (pages with `limit`/`offset`) |
128
255
  | `protect_get_camera` | Get camera details by ID |
129
- | `protect_update_camera` | Update camera settings |
256
+ | `protect_update_camera` | **DESTRUCTIVE:** Update camera settings |
130
257
  | `protect_get_snapshot` | Get a JPEG snapshot (returns image) |
131
- | `protect_create_rtsp_stream` | Create an RTSPS stream session |
132
- | `protect_get_rtsp_streams` | Get active RTSPS stream sessions |
133
- | `protect_delete_rtsp_stream` | Stop and delete an active RTSPS stream |
134
- | `protect_create_talkback` | Create a talkback (two-way audio) session |
135
- | `protect_disable_mic` | **IRREVERSIBLE:** Permanently disable camera microphone |
258
+ | `protect_create_rtsp_stream` | **DESTRUCTIVE:** Create an RTSPS stream session; the returned URLs are credentials for live video (requires `confirm: true`) |
259
+ | `protect_get_rtsp_streams` | Get active RTSPS stream sessions (the URLs are credentials for live video) |
260
+ | `protect_delete_rtsp_stream` | **DESTRUCTIVE:** Stop and delete an active RTSPS stream |
261
+ | `protect_create_talkback` | **DESTRUCTIVE:** Create a talkback (two-way audio) session; the returned URL is a credential (requires `confirm: true`) |
262
+ | `protect_disable_mic` | **DESTRUCTIVE:** Irreversibly disable the camera microphone; only a factory reset restores it (requires `confirm: true`) |
136
263
  | `protect_start_ptz_patrol` | Start PTZ patrol at a given slot |
137
264
  | `protect_stop_ptz_patrol` | Stop PTZ patrol |
138
265
  | `protect_goto_ptz_preset` | Move PTZ to a preset position |
139
266
 
140
- ### Lights (3)
267
+ ### Lights
141
268
  | Tool | Description |
142
269
  |---|---|
143
- | `protect_list_lights` | List all lights |
270
+ | `protect_list_lights` | List all lights (pages with `limit`/`offset`) |
144
271
  | `protect_get_light` | Get light details by ID |
145
- | `protect_update_light` | Update light settings |
272
+ | `protect_update_light` | **DESTRUCTIVE:** Update light settings |
146
273
 
147
- ### Sensors (3)
274
+ ### Sensors
148
275
  | Tool | Description |
149
276
  |---|---|
150
- | `protect_list_sensors` | List all sensors |
277
+ | `protect_list_sensors` | List all sensors (pages with `limit`/`offset`) |
151
278
  | `protect_get_sensor` | Get sensor details by ID |
152
- | `protect_update_sensor` | Update sensor settings |
279
+ | `protect_update_sensor` | **DESTRUCTIVE:** Update sensor settings (requires `confirm: true` to change alarm, motion or glass-break settings, arm profiles or schedule mode) |
153
280
 
154
- ### Chimes (3)
281
+ ### Chimes
155
282
  | Tool | Description |
156
283
  |---|---|
157
- | `protect_list_chimes` | List all chimes |
284
+ | `protect_list_chimes` | List all chimes (pages with `limit`/`offset`) |
158
285
  | `protect_get_chime` | Get chime details by ID |
159
- | `protect_update_chime` | Update chime settings |
286
+ | `protect_update_chime` | **DESTRUCTIVE:** Update chime settings |
160
287
 
161
- ### Viewers (3)
288
+ ### Viewers
162
289
  | Tool | Description |
163
290
  |---|---|
164
- | `protect_list_viewers` | List all viewers |
291
+ | `protect_list_viewers` | List all viewers (pages with `limit`/`offset`) |
165
292
  | `protect_get_viewer` | Get viewer details by ID |
166
- | `protect_update_viewer` | Update viewer settings |
293
+ | `protect_update_viewer` | **DESTRUCTIVE:** Update viewer settings |
167
294
 
168
- ### Sirens (6)
295
+ ### Sirens
169
296
  | Tool | Description |
170
297
  |---|---|
171
- | `protect_list_sirens` | List all sirens |
298
+ | `protect_list_sirens` | List all sirens (pages with `limit`/`offset`) |
172
299
  | `protect_get_siren` | Get siren details by ID |
173
- | `protect_update_siren` | Update siren settings (name, volume, LED) |
300
+ | `protect_update_siren` | **DESTRUCTIVE:** Update siren settings: name, volume, LED (requires `confirm: true` to change the volume) |
174
301
  | `protect_play_siren` | Activate the siren alarm for a given duration (5/10/20/30s) |
175
- | `protect_stop_siren` | Stop an active siren |
302
+ | `protect_stop_siren` | **DESTRUCTIVE:** Stop an active siren; it silences an alarm (requires `confirm: true`) |
176
303
  | `protect_test_siren_sound` | Test the siren sound for 5 seconds at a given volume |
177
304
 
178
- ### Fobs (3)
305
+ ### Fobs
179
306
  | Tool | Description |
180
307
  |---|---|
181
- | `protect_list_fobs` | List all key fobs |
308
+ | `protect_list_fobs` | List all key fobs (pages with `limit`/`offset`) |
182
309
  | `protect_get_fob` | Get fob details by ID |
183
- | `protect_update_fob` | Update fob settings |
310
+ | `protect_update_fob` | **DESTRUCTIVE:** Update fob settings (requires `confirm: true` to change arm control settings) |
184
311
 
185
- ### Relays (4)
312
+ ### Relays
186
313
  | Tool | Description |
187
314
  |---|---|
188
- | `protect_list_relays` | List all relays |
189
- | `protect_get_relay` | Get relay details by ID |
190
- | `protect_update_relay` | Update relay settings |
191
- | `protect_activate_relay_output` | Set/toggle a relay output channel, with optional pulse duration |
315
+ | `protect_list_relays` | List all relays (pages with `limit`/`offset`) |
316
+ | `protect_get_relay` | Get relay details by ID, including each output's state |
317
+ | `protect_update_relay` | **DESTRUCTIVE:** Update relay settings |
318
+ | `protect_activate_relay_output` | **DESTRUCTIVE:** Turn a relay output channel on or off, with optional pulse duration (requires `confirm: true` and an explicit `state`) |
192
319
 
193
- ### Speakers (4)
320
+ ### Speakers
194
321
  | Tool | Description |
195
322
  |---|---|
196
- | `protect_list_speakers` | List all speakers |
323
+ | `protect_list_speakers` | List all speakers (pages with `limit`/`offset`) |
197
324
  | `protect_get_speaker` | Get speaker details by ID |
198
- | `protect_update_speaker` | Update speaker settings (volume, mic) |
325
+ | `protect_update_speaker` | **DESTRUCTIVE:** Update speaker settings (volume, mic) |
199
326
  | `protect_test_speaker_sound` | Test the speaker sound at a given volume |
200
327
 
201
- ### Bridges (3)
328
+ ### Bridges
202
329
  | Tool | Description |
203
330
  |---|---|
204
- | `protect_list_bridges` | List all bridges |
331
+ | `protect_list_bridges` | List all bridges (pages with `limit`/`offset`) |
205
332
  | `protect_get_bridge` | Get bridge details by ID |
206
- | `protect_update_bridge` | Update bridge settings |
333
+ | `protect_update_bridge` | **DESTRUCTIVE:** Update bridge settings |
207
334
 
208
- ### Link Stations (3)
335
+ ### Link Stations
209
336
  | Tool | Description |
210
337
  |---|---|
211
- | `protect_list_link_stations` | List all link stations (non-alarm-hub gateways) |
338
+ | `protect_list_link_stations` | List all link stations, the non-alarm-hub gateways (pages with `limit`/`offset`) |
212
339
  | `protect_get_link_station` | Get link station details by ID |
213
- | `protect_update_link_station` | Update link station settings |
340
+ | `protect_update_link_station` | **DESTRUCTIVE:** Update link station settings |
214
341
 
215
- ### Alarm Hubs (4)
342
+ ### Alarm Hubs
216
343
  | Tool | Description |
217
344
  |---|---|
218
- | `protect_list_alarm_hubs` | List all alarm hubs |
345
+ | `protect_list_alarm_hubs` | List all alarm hubs (pages with `limit`/`offset`) |
219
346
  | `protect_get_alarm_hub` | Get alarm hub details by ID |
220
- | `protect_update_alarm_hub` | Update alarm hub settings |
221
- | `protect_trigger_alarm_hub_output` | Trigger an alarm hub output channel (sirens, lights, etc.) |
347
+ | `protect_update_alarm_hub` | **DESTRUCTIVE:** Update alarm hub settings |
348
+ | `protect_trigger_alarm_hub_output` | **DESTRUCTIVE:** Turn an alarm hub output channel on or off: sirens, lights, locks (requires `confirm: true` and an explicit `enable`) |
222
349
 
223
- ### Arm Profiles (7) — local alarm manager
350
+ ### Arm Profiles — local alarm manager
224
351
  | Tool | Description |
225
352
  |---|---|
226
- | `protect_list_arm_profiles` | List all arm profiles |
353
+ | `protect_list_arm_profiles` | List all arm profiles (pages with `limit`/`offset`) |
227
354
  | `protect_create_arm_profile` | Create a new arm profile |
228
- | `protect_set_current_arm_profile` | Set the active profile used when arming |
229
- | `protect_update_arm_profile` | Update an arm profile |
230
- | `protect_delete_arm_profile` | **DESTRUCTIVE:** Delete an arm profile by ID |
355
+ | `protect_set_current_arm_profile` | **DESTRUCTIVE:** Set the active profile used when arming (requires `confirm: true`) |
356
+ | `protect_update_arm_profile` | **DESTRUCTIVE:** Update an arm profile (requires `confirm: true` to change schedules, automations, activation delay or recording) |
357
+ | `protect_delete_arm_profile` | **DESTRUCTIVE:** Delete an arm profile by ID (requires `confirm: true`) |
231
358
  | `protect_enable_arm_alarm` | Arm the alarm using the current profile |
232
- | `protect_disable_arm_alarm` | Disarm the alarm |
359
+ | `protect_disable_arm_alarm` | **DESTRUCTIVE:** Disarm the alarm (requires `confirm: true`) |
233
360
 
234
- ### Live Views (4)
361
+ ### Live Views
235
362
  | Tool | Description |
236
363
  |---|---|
237
- | `protect_list_liveviews` | List all live views |
364
+ | `protect_list_liveviews` | List all live views (pages with `limit`/`offset`) |
238
365
  | `protect_get_liveview` | Get live view details by ID |
239
366
  | `protect_create_liveview` | Create a new live view |
240
- | `protect_update_liveview` | Update a live view |
367
+ | `protect_update_liveview` | **DESTRUCTIVE:** Replace a live view (send the full object from `protect_get_liveview`) |
241
368
 
242
- ### Alarm & Files (3)
369
+ ### Alarm & Files
243
370
  | Tool | Description |
244
371
  |---|---|
245
- | `protect_trigger_alarm_webhook` | Trigger an alarm webhook (fires external alarm action) |
246
- | `protect_list_files` | List files by type |
247
- | `protect_upload_file` | Upload a file (base64-encoded) |
372
+ | `protect_trigger_alarm_webhook` | **DESTRUCTIVE:** Trigger an alarm webhook, firing its external alarm action (requires `confirm: true`) |
373
+ | `protect_list_files` | List device asset files (`animations`; pages with `limit`/`offset`) |
374
+ | `protect_upload_file` | Upload a device asset file (base64 in, sent as multipart/form-data) |
248
375
 
249
- ### Users (4)
376
+ ### Users
250
377
  | Tool | Description |
251
378
  |---|---|
252
- | `protect_list_users` | List Protect users (filtered by access permissions) |
379
+ | `protect_list_users` | List Protect users, filtered by the key's access (pages with `limit`/`offset`) |
253
380
  | `protect_get_user` | Get a Protect user by ID |
254
- | `protect_list_ulp_users` | List UniFi Identity (ULP) users with enrolled credentials |
381
+ | `protect_list_ulp_users` | List UniFi Identity (ULP) users with enrolled credentials (pages with `limit`/`offset`) |
255
382
  | `protect_get_ulp_user` | Get a UniFi Identity user by ID |
256
383
 
257
- ### Point of Sale (1)
384
+ ### Point of Sale
258
385
  | Tool | Description |
259
386
  |---|---|
260
387
  | `protect_ingest_pos_transaction` | Record a POS transaction as a camera event so it can be overlaid on recorded footage |
261
388
 
389
+ ## Troubleshooting
390
+
391
+ Configuration errors go to the MCP host's server log and the server exits. Tool errors reach the agent as results starting with `Error:`.
392
+
393
+ | Message | Cause and fix |
394
+ |---|---|
395
+ | `Configuration error: UNIFI_PROTECT_HOST is required` / `UNIFI_PROTECT_API_KEY is required` | Missing from the server's `env`. |
396
+ | `UNIFI_PROTECT_HOST must be a hostname or IP …` | A path, credentials or a bad port in the host; bracket IPv6 addresses. |
397
+ | `UNIFI_PROTECT_API_KEY contains a control character …` | Paste the key again. |
398
+ | `cannot read UNIFI_PROTECT_CA_CERT (…)` | The path does not exist for the server. Use an absolute path; `~` is not expanded. |
399
+ | `UNIFI_PROTECT_CA_CERT must name a PEM file containing a certificate` | No `BEGIN CERTIFICATE` block; run the `openssl` command again. |
400
+ | `…cannot be combined with UNIFI_PROTECT_VERIFY_SSL=false` / `TLS verification cannot be disabled for api.ui.com` / `UNIFI_PROTECT_CA_CERT is for local consoles` | Unset `UNIFI_PROTECT_VERIFY_SSL` (or `UNIFI_PROTECT_CA_CERT` for `api.ui.com`). |
401
+ | `Ignoring UNIFI_PROTECT_CONSOLE_ID` (log line) | The console ID only applies with `UNIFI_PROTECT_HOST=api.ui.com`. |
402
+ | `… (read-only: write tools are not registered)` (log line) and no write tools | Read-only is the default. Set `UNIFI_PROTECT_READ_ONLY=false` and restart the host. |
403
+ | `fetch failed: … The console's TLS certificate could not be verified …` | Save the console's certificate to `UNIFI_PROTECT_CA_CERT`. If you already did, the console has a new certificate: save it again. |
404
+ | `fetch failed: … The certificate for api.ui.com could not be verified …` | Something between you and Ubiquiti's cloud is presenting another certificate, such as a TLS-inspecting proxy or firewall. |
405
+ | `fetch failed: connect ECONNREFUSED …`, `ENOTFOUND` | Wrong or unreachable host. |
406
+ | `HTTP 401` / `HTTP 403` | Wrong or revoked key, or its admin lacks the rights. |
407
+ | `… Protect did not respond within … s` | The console is slow. For a write, check whether it was applied before retrying. |
408
+ | `… exceeds the …-byte limit` / `Result too large to send in one MCP message …` | The response is too large for one MCP message. Page a list with `limit` and `offset`; for a snapshot, drop `highQuality`. |
409
+ | `File too large to send in one MCP message` | Upload a smaller file. |
410
+ | `This action requires confirmation: …` | Expected: the action needs `confirm: true`, and the agent should ask you first. |
411
+ | `Use an ID returned by a list or get tool` | The agent passed a name or a malformed ID. |
412
+ | `Subscription failed: …` | The WebSocket was refused or unauthorized; this is not "no activity". |
413
+ | `WebSocket subscriptions are not available over Cloud Connector …` | Connect to the console directly for subscriptions. |
414
+ | `MCP error -32602 … Input validation error` | The call broke the tool's input schema (for example a missing `state`); the message names the field. |
415
+
262
416
  ## Development
263
417
 
264
418
  The Node version used for development is pinned in `.nvmrc`. With [fnm](https://github.com/Schniz/fnm) installed, `fnm use` reads it automatically on `cd`.
@@ -266,15 +420,16 @@ The Node version used for development is pinned in `.nvmrc`. With [fnm](https://
266
420
  ```bash
267
421
  pnpm run build # Compile TypeScript
268
422
  pnpm start # Run the server
269
- pnpm run typecheck # Type-check without emitting
270
- pnpm run lint # ESLint
423
+ pnpm run typecheck # Type-check src/ and tests/ without emitting
424
+ pnpm run lint # ESLint, type-aware
271
425
  pnpm run lint:fix # ESLint with auto-fix
272
426
  pnpm test # Run all tests (vitest)
273
427
  pnpm run test:watch # Run tests in watch mode
274
428
  pnpm run test:coverage # Run tests with coverage
429
+ pnpm run test:package # Pack, install the tarball and smoke-test it over stdio
275
430
  ```
276
431
 
277
- Git hooks are managed by [lefthook](https://github.com/evilmartians/lefthook) — `pnpm exec lefthook install` enables them. Pre-commit runs ESLint on staged files and a full typecheck, so fnm and pnpm both need to be on your PATH.
432
+ Git hooks are managed by [lefthook](https://github.com/evilmartians/lefthook) — `pnpm exec lefthook install` enables them. Pre-commit runs ESLint on staged files, a full typecheck, and a GitGuardian secret scan when [ggshield](https://github.com/GitGuardian/ggshield) is installed (skipped otherwise), so fnm and pnpm both need to be on your PATH.
278
433
 
279
434
  ### Commit conventions
280
435
 
@@ -282,10 +437,11 @@ This project uses [conventional commits](https://www.conventionalcommits.org/) a
282
437
 
283
438
  - `feat: ...` — new feature (minor version bump)
284
439
  - `fix: ...` — bug fix (patch version bump)
440
+ - `deps: ...` — dependency update (patch version bump; Renovate's type for package bumps)
285
441
  - `feat!: ...` or `BREAKING CHANGE:` footer — breaking change (major version bump)
286
442
  - `chore:`, `docs:`, `ci:`, etc. — no version bump
287
443
 
288
- On push to `main`, release-please opens a Release PR that bumps the version and updates `CHANGELOG.md`. Merging that PR publishes to npm automatically.
444
+ On push to `main`, release-please opens a Release PR that bumps the version and updates `CHANGELOG.md`. Merging that PR publishes to npm automatically. Merge pull requests with a rebase merge, so each commit (and any `BREAKING CHANGE` footer) reaches the changelog.
289
445
 
290
446
  To override the version number, add `Release-As: x.x.x` in the commit body:
291
447
 
package/dist/client.d.ts CHANGED
@@ -1,19 +1,59 @@
1
1
  import WebSocket from "ws";
2
2
  import { Config } from "./config.js";
3
+ /** Run fn so that every request it makes is aborted when signal is. */
4
+ export declare function withCallSignal<T>(signal: AbortSignal | undefined, fn: () => T): T;
5
+ /** The abort signal of the tool call in progress, if any. */
6
+ export declare function currentCallSignal(): AbortSignal | undefined;
7
+ /**
8
+ * The reason an error carries, for a message the model reads. A connection
9
+ * that tried several addresses (a name with both IPv4 and IPv6 records) fails
10
+ * with an AggregateError whose own message is empty; its reasons are in
11
+ * `errors`, and its code in `code`.
12
+ */
13
+ export declare function errorReason(err: Error): string;
3
14
  export declare class ProtectClient {
4
15
  private baseUrl;
16
+ private basePath;
17
+ private tls;
18
+ private dispatcher;
5
19
  private headers;
6
20
  private usingConnector;
7
21
  constructor(config: Config);
22
+ /** Every header but Content-Type, for a request whose body sets its own. */
23
+ private authHeaders;
24
+ /**
25
+ * Model-supplied IDs become path segments. safePath encodes `/ ? # % \`, but
26
+ * a bare `.` or `..` survives encoding and URL parsing then folds it out of
27
+ * the path, so the request would reach a different endpoint than the tool
28
+ * built. Refuse any path that parsing would rewrite. The ID schemas reject
29
+ * these first; this is the backstop for a tool that forgets one.
30
+ */
31
+ private resolve;
32
+ /**
33
+ * The one place a request is sent: every path builds its URL, applies
34
+ * `redirect: "error"` (so X-API-KEY never follows a redirect) and its
35
+ * timeout, and turns a non-2xx status into an error with a bounded body.
36
+ */
37
+ private send;
8
38
  private request;
39
+ private sendJson;
9
40
  get(path: string): Promise<unknown>;
10
41
  post(path: string, body?: unknown): Promise<unknown>;
11
42
  patch(path: string, body: unknown): Promise<unknown>;
12
43
  delete(path: string): Promise<unknown>;
13
- getBinary(path: string): Promise<{
44
+ getBinary(path: string, maxBytes?: number): Promise<{
14
45
  data: Buffer;
15
46
  mimeType: string;
16
47
  }>;
17
- postBinary(path: string, data: Buffer, contentType: string): Promise<unknown>;
18
- connectWebSocket(path: string): WebSocket;
48
+ private download;
49
+ /**
50
+ * Upload a file as multipart/form-data (the documented encoding for
51
+ * POST /files/{fileType}). fetch sets the Content-Type with its boundary, so
52
+ * none is set here. The part name `file` is not in the published docs but
53
+ * was verified against a live 7.3.70 console.
54
+ */
55
+ postFile(path: string, data: Buffer, contentType: string, fileName: string): Promise<unknown>;
56
+ connectWebSocket(path: string, options?: {
57
+ maxPayload?: number;
58
+ }): WebSocket;
19
59
  }