Skip to content
SZ-MCP
Get Support

Code mode primitives

Both namespaces expose the same eight primitives. wifi operates on the SmartZone WSG API, switches on SwitchM.

PrimitivePurposeNetwork call
list_tagsTags with operation countsNo
search_endpointsKeyword search over the indexNo
list_endpoints_by_tagEvery endpoint under a tagNo
get_endpoint_detailsFull schema for one endpointNo call to your controller; the full spec is loaded from storage on a cache miss
callExecute an authenticated requestYes
save_endpoint_noteSave a quirk you proved about an endpoint, for your organisationNo
remove_endpoint_noteRemove an endpoint noteNo
await_approvalWait for a dashboard approval of a dangerous writeNo

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.

Takes no arguments. This is the starting point for discovery.

const { tags } = await switches.list_tags();
ReturnsType
tagsArray 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.

const { results, count } = await wifi.search_endpoints({ query: 'wlan profile' });
ArgumentTypeDefaultNotes
querystring—Required. Minimum 2 characters
tagstring—Restrict to one tag, exact match
methodenum—GET, POST, PUT, PATCH, DELETE
limitnumber20Accepted range 1–50
ReturnsType
resultsArray of index entries, ranked best first
countTotal 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.

const { results, count } = await switches.list_endpoints_by_tag({ tag: 'Switch Health' });
ArgumentTypeDefaultNotes
tagstring—Required. Exact name from list_tags
methodenum—Filter by HTTP method
limitnumber50Accepted range 1–200
offsetnumber0For paging through a large tag
ReturnsType
resultsIndex entries for this page
countTotal 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.

Both search and browse return entries of this shape:

FieldNotes
methodUpper-case HTTP method
pathSpec path, with {placeholder} segments
operationIdPresent on every operation
tagsTag names this operation belongs to
summaryPresent on every operation; cut to 120 characters in results (get_endpoint_details has it in full)
descriptionAlways absent — the specs populate none
paramsSummaryParameter names grouped by location, e.g. path: id; query: listSize,index
hasBodytrue 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'
paginationStylelistSize-index, page-limit-body, page-limit-query or none
requiredParams{ path, query, header } — the required parameter names
requestBodyHintThe 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
responseHintThe response’s fields, plus listFields for the items of a { list, hasMore, totalCount } envelope
caveatsPresent when the endpoint has quirks verified on a live controller. They override the spec
notesCountPresent when people saved endpoint notes on it
deprecatedPresent only when true. No operation in either spec is marked deprecated
const details = await wifi.get_endpoint_details({ method: 'POST', path: '/rkszones' });
ArgumentTypeNotes
methodenumRequired
pathstringRequired. Must start with / and be the spec path, with its {placeholder} segments
ReturnsField
surface, method, pathEchoed back
operationId, summary, tagsFrom the spec
description, deprecatedOnly when the spec sets them — in practice, never
access, paginationStyleAs on the index entry
parametersPath, 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
responsesResponse definitions by status code
caveatsQuirks verified on a live controller, when there are any. They override the spec
notesEndpoint notes saved by your organisation (and any shared with every organisation), when there are any: [{ id, scope: 'org' | 'global', note, by?, at }]
notePresent 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.

Unlike call, this primitive returns a plain object on failure:

ReturnMeaning
{ 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 };
ArgumentTypeNotes
methodenumRequired. GET, POST, PUT, PATCH, DELETE
pathstringRequired. Spec path, starting /, no base prefix
pathParamsobjectValues substituted into {placeholder} segments, URL-encoded
queryobjectQuery parameters. Arrays append the key once per value
bodyanyJSON-serialised unless already a string. Ignored on GET and DELETE
headersobjectOnly 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)
retryOn429booleanAlso retry a POST or PATCH on 429 (see Automatic retry)
confirmTokenstringFrom a confirm_required response, after you agreed to the previewed write
totpCodestringThe 6-digit code from your authenticator app, for a dangerous write
approvalIdstringFrom 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.

FieldNotes
oktrue for 2xx
statusHTTP status code
statusTextHTTP status text
headersResponse headers as an object
bodyParsed 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 }
contentTypeThe response’s Content-Type
durationMsTime for the request, including any 401 or 429 retry
urlThe upstream URL with serviceTicket=<redacted>
retriedHow many 429 retries were made (0 when none)
budget{ calls, totalCallMs, elapsedMs, budgetMs, warning? } for this run. warning appears from 20 seconds
writeIdOn a write that was sent: its id in the write history (history.get_write, history.undo)
verificationOn a write: { watching: true, dueAt, see } while SZ-MCP watches what it touched for 15 minutes, or { skipped }
guardOn 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.

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.

When the failure happens before the request reaches SmartZone, call still returns rather than throwing — with status: 0:

statusTextbody.errorMeaning
sz_auth_errorno_credentialsNo 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_errorlogin_failedThe controller rejected the login, e.g. http_401
sz_auth_errorforbidden_hostThe saved host is not publicly routable, or not a bare host name or IP address
sz_auth_errorforbidden_portThe saved port is not 8443 or 443
sz_auth_errorinvalid_hostController unreachable at login — DNS, timeout, refused connection, or TLS validation failure
sz_auth_errordecryption_failedStored password could not be decrypted
sz_auth_errortunnel_down, tunnel_origin_unreachable, tunnel_access_deniedOnly with a controller tunnel: Cloudflare’s edge, not the controller, answered
invalid_pathinvalid_pathThe path, a header or a query key was refused before sending. body.message says why
network_errornetworkUnexpected 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:

statusTextMeaning
confirm_requiredNot sent. Show the user guard.preview; after they agree, re-send the identical call with confirmToken: guard.confirmToken
totp_requiredA dangerous write: re-send with the same confirmToken and the user’s totpCode
totp_invalidThe code was wrong, expired or already used. Ask for a fresh one
totp_lockedToo many codes tried in the last 5 minutes. Do not retry now
approval_requiredA dangerous write by someone with no authenticator app: give the user guard.approvalUrl, then await_approval
approval_pendingNot approved on the dashboard yet
approval_deniedDenied on the dashboard. Do not retry unless the user asks again
write_blockedThe 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.

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.' });
ArgumentNotes
method, pathRequired. The spec operation
noteRequired. 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.

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).

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.