MCP integration
If you are building an AI agent, you do not need to wrap the REST API yourself. The platform exposes an MCP (Model Context Protocol) endpoint that presents cloud phone queries and operations as a set of tools. Any MCP-capable client can call them once you configure the address and a key.
Prefer plain HTTP? See the overview and the endpoint list. Both paths use the same applications and permissions; only the authentication differs.
Endpoint
POST https://<your-domain>/mcp
- Transport: Streamable HTTP, stateless — no session to maintain
- Protocol version:
2025-03-26 - Server identity:
nodemobi-cloudphone
A GET to this address returns 405 and tells you to use POST. That is
expected: this endpoint has no server-push stream. It is not a fault. In
practice, mainstream clients only ever send POST.
Authentication
Keys (key_id and secret) are issued per application in the developer
console. The two forms below are equivalent — use whichever your client
supports.
Two request headers
X-Api-Key: ak_xxxxxxxxxxxx
X-Api-Secret: cps_xxxxxxxxxxxx
Or a single bearer token
Authorization: Bearer ak_xxxxxxxxxxxx:cps_xxxxxxxxxxxx
Separated by a colon. Neither key_id nor secret contains a colon, so
splitting on the first one is unambiguous. The bearer form exists because many
MCP clients can only be configured with a single token and cannot send
arbitrary headers.
If both forms are present, the explicit headers win.
/mcp and /v1 do not authenticate the same way. /v1 uses HMAC signing
(X-Api-Key + X-Timestamp + X-Nonce + X-Signature); sending the MCP
bearer token to /v1 returns 401.
Your first call
Before configuring a client, confirm the key works with a single command.
tools/list is read-only and touches none of your devices.
curl -s -X POST https://<your-domain>/mcp \
-H "Authorization: Bearer $NODEMOBI_MCP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Two notes for Windows:
- In PowerShell,
curlis an alias forInvoke-WebRequest, which does not accept-Hand fails with a parameter-binding error. Writecurl.exeto get the real one. - Or use the native PowerShell form:
$h = @{ Authorization = "Bearer $env:NODEMOBI_MCP_TOKEN"; 'Content-Type' = 'application/json' }
$r = Invoke-RestMethod -Method Post -Uri 'https://<your-domain>/mcp' -Headers $h `
-Body '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
$r.result.tools.name
The response lists the tools your key can call. A list shorter than the
table below is expected — tools/list only returns tools this key has
permission for.
Client configuration
Clients with native Streamable HTTP support
Point them at the URL and keep the key in an environment variable. Codex CLI, for example:
codex mcp add nodemobi \
--url https://<your-domain>/mcp \
--bearer-token-env-var NODEMOBI_MCP_TOKEN
Then set NODEMOBI_MCP_TOKEN to ak_xxx:cps_xxx. The config file holds only
the variable name, so the key never touches disk.
Clients that only speak stdio, or cannot set custom headers
Bridge through mcp-remote locally. A Claude Desktop config looks like this:
{
"mcpServers": {
"nodemobi": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://<your-domain>/mcp",
"--header", "X-Api-Key:${NODEMOBI_KEY}",
"--header", "X-Api-Secret:${NODEMOBI_SECRET}",
"--transport", "http-only"],
"env": {
"NODEMOBI_KEY": "ak_xxxxxxxxxxxx",
"NODEMOBI_SECRET": "cps_xxxxxxxxxxxx"
}
}
}
}
This form stores the key in plain text in a config file. Use native Streamable HTTP where you can. Where you cannot, grant that key read-only permissions, track it separately, and be ready to revoke it.
Tools and permissions
tools/list returns only the tools this key can actually call. Tools the
key lacks permission for are omitted, so an agent never calls an endpoint that
is certain to fail.
| Tool | Purpose | Permission |
|---|---|---|
list_devices | List devices and basic state | device_read |
get_device | Read one device's state, spec and capabilities | device_read |
get_task | Read task status | device_read |
create_session | Issue a short-lived H5 screen session token | stream |
set_proxy | Set an egress proxy | device |
clear_proxy | Clear the proxy, restore direct connection | device |
set_power | Boot / shutdown / restart | device |
device implies device_read; you do not need to grant both.
Omitting a tool from the list is advance notice, not the permission check itself: calling a tool by name without permission is refused just the same.
Some clients prefix tool names — in Codex the name is actually
mcp__nodemobi__list_devices. If you cannot find a tool, print your client's
full tool list before concluding the server is missing.
Each tool's full contract (arguments, preconditions, how to handle failure) is
in the description returned by tools/list, which your client reads
directly. It is not repeated here.
Check capabilities before calling
Every device returned by list_devices and get_device carries
capabilities:
{
"device_id": "1",
"state": "running",
"capabilities": { "proxy": true, "power": true, "session": true }
}
It reports whether the platform can currently perform that operation on
that device. Check it before calling a write tool; if it is false, do not
call.
A state of unknown means the upstream state could not be read. It does
not mean the device is not yours, and it does not mean the device is
faulty. Decide what you can do from capabilities, never by inferring from
state.
Write operations are asynchronous
Every write tool except create_session returns a task, not a final result:
{ "task_id": "tsk_xxxx", "status": "dispatched", "terminal": false }
dispatched means dispatched, not applied. Poll get_task until
terminal: true to learn the outcome, and do not poll faster than every 15
seconds.
The five terminal states
They mean different things and call for different handling. Do not collapse them into success or failure:
| Terminal state | Meaning |
|---|---|
success | The effect is confirmed — the platform verified it on the device |
success_unverified | Upstream reported success, but the platform has no way to verify this kind of operation. Not the same as success — only "upstream says so" |
failed | Definitely failed: upstream returned an error, or verification confirmed no effect and upstream agrees |
failed_silent | Upstream reported success but the effect is measurably absent. The only state that triggers a refund, and the one most worth alerting on |
timeout | The expected effect could not be read within the deadline, and it cannot be concluded that nothing happened (the device may simply be unreachable). No refund, and it should not be treated as failure |
For example, clear_proxy has no exit IP to compare against, so its normal
terminal state is usually success_unverified or timeout rather than
success.
Idempotency
A write tool returning idempotent_hit: true means the request shares an
idempotency key with a task that is still in flight. You get that existing
task back and nothing was dispatched twice — this is not a failure of your
call.
Once the earlier task reaches a terminal state, the same operation can be issued again.
Handling errors
Failure responses carry a retryable field. Let it decide:
{ "code": "not_supported", "message": "This device does not support proxying", "retryable": false }
retryable: false— terminal. Do not retry, do not back off. Move to another device or report it.retryable: true— worth retrying with backoff.
Two codes that are easy to confuse:
forbidden— this key lacks the permission the tool requires. Grant the application that permission in the console.not_supported— permissions are fine; this device does not offer the capability.capabilitieslets you avoid it up front.
Rate limit
300 requests per minute by default, counted per application (not per
key — multiple keys of one application share the budget). Exceeding it returns
rate_limited with retryable: true.
Current limitations
Stated up front so you do not design around wrong assumptions:
- No callbacks. Task completion is not pushed to you; poll
get_task. create_sessionmay returnexpires_at: null, meaning upstream gave no expiry. Do not assume the token is long-lived — reissue it when it stops working.- Capabilities differ between devices. Trust
capabilities; do not assume every device in your account behaves alike.