Code mode primitives
Both namespaces expose the same eight primitives. wifi operates on the
SmartZone WSG API, switches on SwitchM.
| Primitive | Purpose | Network call |
|---|---|---|
list_tags | Tags with operation counts | No |
search_endpoints | Keyword search over the index | No |
list_endpoints_by_tag | Every endpoint under a tag | No |
get_endpoint_details | Full schema for one endpoint | No call to your controller; the full spec is loaded from storage on a cache miss |
call | Execute an authenticated request | Yes |
save_endpoint_note | Save a quirk you proved about an endpoint, for your organisation | No |
remove_endpoint_note | Remove an endpoint note | No |
await_approval | Wait for a dashboard approval of a dangerous write | No |
The first three read the index bundled into the Worker, so they are effectively
free. get_endpoint_details loads the full spec from object storage the first
time it is used for a surface in a given isolate. Only call reaches your
controller.
Beyond these, the two namespaces differ: each carries its own task helpers
(on wifi, for example, list_all, query, overview, alarms, events,
zone_tree, client_journey, ap_ping; on switches, list_all,
overview, config_diff, backups, config, backup, restore,
port_lookup, routing_table, ping, traceroute; and get_reference,
search_docs and read_doc on both). Ten more namespaces — stream,
inventory, metrics, alerts, boards, history, notes, mop, lint
and org — carry helpers of their own. The code_mode tool description lists
every helper under TASK HELPERS, and its workflow tells Claude to check that
list before searching the API. See How code mode works.
list_tags
Section titled “list_tags”Takes no arguments. This is the starting point for discovery.
const { tags } = await switches.list_tags();| Returns | Type |
|---|---|
tags | Array of { name, description?, operationCount } |
Returns every tag on the surface — 117 on wifi, 42 on switches.
description is absent on every tag for the same reason the specs carry no
operation descriptions.
There is no tag-group level above tags. An earlier list_tag_groups primitive,
and a group argument on list_tags, were removed because the SmartZone specs
define no tag groups, so both could only return an empty list.
search_endpoints
Section titled “search_endpoints”const { results, count } = await wifi.search_endpoints({ query: 'wlan profile' });| Argument | Type | Default | Notes |
|---|---|---|---|
query | string | — | Required. Minimum 2 characters |
tag | string | — | Restrict to one tag, exact match |
method | enum | — | GET, POST, PUT, PATCH, DELETE |
limit | number | 20 | Accepted range 1–50 |
| Returns | Type |
|---|---|
results | Array of index entries, ranked best first |
count | Total matches before the limit was applied |
Query text is lower-cased and split on whitespace; each token is matched as a
substring of the entry’s search blob (operationId, tags, summary, path, method,
and the request and response field names). Per token found there, scoring adds
+1, then +2 for a hit in operationId, +1 for a tag name, +1 for the path and
+1 more when the token is a whole path segment. A multi-word query that matches
every token gets a bonus equal to the number of tokens. Ties go to the shorter
path.
Because count is the pre-limit total, a count far above results.length
means narrowing the query or adding a tag will serve you better than raising
limit.
list_endpoints_by_tag
Section titled “list_endpoints_by_tag”const { results, count } = await switches.list_endpoints_by_tag({ tag: 'Switch Health' });| Argument | Type | Default | Notes |
|---|---|---|---|
tag | string | — | Required. Exact name from list_tags |
method | enum | — | Filter by HTTP method |
limit | number | 50 | Accepted range 1–200 |
offset | number | 0 | For paging through a large tag |
| Returns | Type |
|---|---|
results | Index entries for this page |
count | Total under the tag, ignoring limit and offset |
list_tags counts the operations in the vendored spec. A few operations
SZ-MCP adds itself, because they work but the spec leaves them out, are not in
those counts: POST /audit/list (tag Administrator Audit, which list_tags
does not list) on wifi, and POST /switch/troubleshooting/ping and
/traceroute on switches, which make Switch Troubleshooting browse to 6
operations against a list_tags count of 4.
Tag matching is exact and case-sensitive — pass the name verbatim as
list_tags returned it.
Index entry fields
Section titled “Index entry fields”Both search and browse return entries of this shape:
| Field | Notes |
|---|---|
method | Upper-case HTTP method |
path | Spec path, with {placeholder} segments |
operationId | Present on every operation |
tags | Tag names this operation belongs to |
summary | Present on every operation; cut to 120 characters in results (get_endpoint_details has it in full) |
description | Always absent — the specs populate none |
paramsSummary | Parameter names grouped by location, e.g. path: id; query: listSize,index |
hasBody | true when the operation takes a request body (484 on wifi, 142 on switches) |
access | 'read' or 'write' — how the write guard classifies it. SwitchM list endpoints are POSTs whose operationId says add…; they are 'read' |
paginationStyle | listSize-index, page-limit-body, page-limit-query or none |
requiredParams | { path, query, header } — the required parameter names |
requestBodyHint | The body’s field names (props, each with required when it is), and ref, the schema name. Often enough to call the endpoint without get_endpoint_details |
responseHint | The response’s fields, plus listFields for the items of a { list, hasMore, totalCount } envelope |
caveats | Present when the endpoint has quirks verified on a live controller. They override the spec |
notesCount | Present when people saved endpoint notes on it |
deprecated | Present only when true. No operation in either spec is marked deprecated |
get_endpoint_details
Section titled “get_endpoint_details”const details = await wifi.get_endpoint_details({ method: 'POST', path: '/rkszones' });| Argument | Type | Notes |
|---|---|---|
method | enum | Required |
path | string | Required. Must start with / and be the spec path, with its {placeholder} segments |
| Returns | Field |
|---|---|
surface, method, path | Echoed back |
operationId, summary, tags | From the spec |
description, deprecated | Only when the spec sets them — in practice, never |
access, paginationStyle | As on the index entry |
parameters | Path, query and header parameters. The body, upload fields and serviceTicket are not here |
requestBody | { required, contentType, schema }, or { required, contentType, fields } for a form upload; null when the operation takes no body |
responses | Response definitions by status code |
caveats | Quirks verified on a live controller, when there are any. They override the spec |
notes | Endpoint notes saved by your organisation (and any shared with every organisation), when there are any: [{ id, scope: 'org' | 'global', note, by?, at }] |
note | Present when the stored full spec is a different version from the search index: the two may disagree |
The vendored specs are Swagger 2.0, which declares a body as a parameter
with in: "body". get_endpoint_details moves it out of parameters and into
requestBody, so read the body schema like this:
const schema = details.requestBody?.schema;$ref pointers are inlined with a budget of four references along any one
path from the root; plain nested objects cost nothing, so a body’s own fields
are always shown. A reference left unexpanded because the budget ran out reads
{ $ref: "…", unresolved: "depth" }, and one that would repeat a reference
already on its path (a cycle) is left as { $ref: "…" }. allOf branches are
merged into one object.
serviceTicket is removed from parameters. Do not pass it — SZ-MCP attaches
it server-side.
Error returns
Section titled “Error returns”Unlike call, this primitive returns a plain object on failure:
| Return | Meaning |
|---|---|
{ error: 'not_found', method, path } | No such operation in the bundled index |
{ error: 'details_unavailable', method, path } | In the index, but absent from the stored full spec |
The only primitive that reaches your controller.
const r = await wifi.call({ method: 'GET', path: '/rkszones/{zoneId}/wlans', pathParams: { zoneId: 'abc-123' }, query: { listSize: 100, index: 0 },});if (!r.ok) return { failed: r.status, body: r.body };| Argument | Type | Notes |
|---|---|---|
method | enum | Required. GET, POST, PUT, PATCH, DELETE |
path | string | Required. Spec path, starting /, no base prefix |
pathParams | object | Values substituted into {placeholder} segments, URL-encoded |
query | object | Query parameters. Arrays append the key once per value |
body | any | JSON-serialised unless already a string. Ignored on GET and DELETE |
headers | object | Only Accept, Content-Type and Accept-Language. Any other header is refused, and so is a method override (an X-HTTP-Method-Override-style header or a _method query key) |
retryOn429 | boolean | Also retry a POST or PATCH on 429 (see Automatic retry) |
confirmToken | string | From a confirm_required response, after you agreed to the previewed write |
totpCode | string | The 6-digit code from your authenticator app, for a dangerous write |
approvalId | string | From an approval_required response, once you approved it on the dashboard |
Accept: application/json is sent always; Content-Type: application/json is
added for requests that carry a body.
path may not contain ?, #, % escapes, backslashes, ;, empty segments
(// or a trailing /) or dot segments: put values in pathParams and
query. A {placeholder} is refused where the matching spec operation has
fixed text, and on a path the spec does not know at all.
Return shape
Section titled “Return shape”| Field | Notes |
|---|---|
ok | true for 2xx |
status | HTTP status code |
statusText | HTTP status text |
headers | Response headers as an object |
body | Parsed JSON when the response is JSON; raw text otherwise; null when empty. PEM private-key blocks are replaced with [private key redacted]. A body over 32 MB is replaced by { error: 'response_too_large', message, preview } |
contentType | The response’s Content-Type |
durationMs | Time for the request, including any 401 or 429 retry |
url | The upstream URL with serviceTicket=<redacted> |
retried | How many 429 retries were made (0 when none) |
budget | { calls, totalCallMs, elapsedMs, budgetMs, warning? } for this run. warning appears from 20 seconds |
writeId | On a write that was sent: its id in the write history (history.get_write, history.undo) |
verification | On a write: { watching: true, dueAt, see } while SZ-MCP watches what it touched for 15 minutes, or { skipped } |
guard | On a write that was not sent: what to do next (see Error envelopes) |
Response headers come back without set-cookie. Redirects are returned as they
are, never followed.
Automatic retry
Section titled “Automatic retry”A 401 discards the cached service ticket, mints a fresh one and retries
exactly once. A second 401 is returned to you.
A 429 is retried up to 2 times for GET, PUT and DELETE, and for
POST or PATCH only with retryOn429: true. Each retry waits for the
Retry-After header (seconds or a date), capped at 5 seconds, or 1 second when
there is none. retried says how many were made. No other status is retried.
Error envelopes
Section titled “Error envelopes”When the failure happens before the request reaches SmartZone, call still
returns rather than throwing — with status: 0:
statusText | body.error | Meaning |
|---|---|---|
sz_auth_error | no_credentials | No controller saved for this organisation. body.message: “No SmartZone controller is saved for this organisation. An admin can save one on the dashboard.” |
sz_auth_error | login_failed | The controller rejected the login, e.g. http_401 |
sz_auth_error | forbidden_host | The saved host is not publicly routable, or not a bare host name or IP address |
sz_auth_error | forbidden_port | The saved port is not 8443 or 443 |
sz_auth_error | invalid_host | Controller unreachable at login — DNS, timeout, refused connection, or TLS validation failure |
sz_auth_error | decryption_failed | Stored password could not be decrypted |
sz_auth_error | tunnel_down, tunnel_origin_unreachable, tunnel_access_denied | Only with a controller tunnel: Cloudflare’s edge, not the controller, answered |
invalid_path | invalid_path | The path, a header or a query key was refused before sending. body.message says why |
network_error | network | Unexpected failure, including the 15-second request timeout; body.message carries the detail |
A write the write guard did not
send also comes back with ok: false and status: 0, statusText naming the
next step, and the details in guard:
statusText | Meaning |
|---|---|
confirm_required | Not sent. Show the user guard.preview; after they agree, re-send the identical call with confirmToken: guard.confirmToken |
totp_required | A dangerous write: re-send with the same confirmToken and the user’s totpCode |
totp_invalid | The code was wrong, expired or already used. Ask for a fresh one |
totp_locked | Too many codes tried in the last 5 minutes. Do not retry now |
approval_required | A dangerous write by someone with no authenticator app: give the user guard.approvalUrl, then await_approval |
approval_pending | Not approved on the dashboard yet |
approval_denied | Denied on the dashboard. Do not retry unless the user asks again |
write_blocked | The person’s role is read-only, or read-only mode is on. Do not retry |
url is an empty string on all of these, since no request was made.
save_endpoint_note
Section titled “save_endpoint_note”Saves a short note on an endpoint: a quirk proved by calling it that the spec,
its caveats and the guides don’t say. Everyone in your organisation then sees it
as notes on get_endpoint_details and notesCount on search and browse
results. Operator role and up.
await wifi.save_endpoint_note({ method: 'POST', path: '/query/ap', note: 'STATUS in filters returns 400; filter on the returned status field.' });| Argument | Notes |
|---|---|
method, path | Required. The spec operation |
note | Required. 5–500 characters. Facts only, no names, addresses or secrets from your network |
Returns { ok, id }. Refusals: note_too_short, note_too_long,
too_many_notes_for_endpoint (10 per endpoint), too_many_notes (500 per
organisation), endpoint_not_found, and write_blocked for a viewer.
remove_endpoint_note
Section titled “remove_endpoint_note”await wifi.remove_endpoint_note({ id: 12 });Removes a note by the id shown in notes: your own, or any of your
organisation’s if you are an org admin. Returns { ok }, or not_found,
not_author, or global (a note shared with every organisation, which only the
site admin can remove).
await_approval
Section titled “await_approval”const s = await wifi.await_approval({ approvalId });Waits up to 15 seconds for the user to approve or deny a dangerous write on
the dashboard, given the approvalId from an approval_required response.
Returns { status }: 'pending', 'approved', 'denied', 'used',
'expired' or 'not_found'. Once approved, re-send the identical call with
approvalId.