Skip to main content

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.

caution

/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, curl is an alias for Invoke-WebRequest, which does not accept -H and fails with a parameter-binding error. Write curl.exe to 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"
}
}
}
}
warning

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.

ToolPurposePermission
list_devicesList devices and basic statedevice_read
get_deviceRead one device's state, spec and capabilitiesdevice_read
get_taskRead task statusdevice_read
create_sessionIssue a short-lived H5 screen session tokenstream
set_proxySet an egress proxydevice
clear_proxyClear the proxy, restore direct connectiondevice
set_powerBoot / shutdown / restartdevice

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.

note

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 stateMeaning
successThe effect is confirmed — the platform verified it on the device
success_unverifiedUpstream reported success, but the platform has no way to verify this kind of operation. Not the same as success — only "upstream says so"
failedDefinitely failed: upstream returned an error, or verification confirmed no effect and upstream agrees
failed_silentUpstream reported success but the effect is measurably absent. The only state that triggers a refund, and the one most worth alerting on
timeoutThe 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. capabilities lets 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_session may return expires_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.