> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mobilerun.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Mobilerun Assistant · MCP

> Teach an agent to chat with the hosted Mobilerun virtual assistant through MCP — send tasks, poll replies, and resolve question and approval cards.

The **Assistant · MCP skill** teaches an agent with the Mobilerun MCP server connected to chat with the hosted virtual assistant (the VA) through the `assistant` tool. Send a natural-language task, poll its progress, and handle the human-in-the-loop (**HITL**) cards it raises.

<Note>
  For HTTP/curl or SDK access, use [Assistant · chat](/skills/assistant). For connection setup, see [MCP server](/mcp-server). For direct phone control, use [OpenClaw · Android control](/skills/openclaw).
</Note>

## Install

Download the packaged skill and load it into a compatible agent runtime:

<Card title="mobilerun-assistant-mcp.skill" icon="download" href="https://github.com/droidrun/skills/releases/latest/download/mobilerun-assistant-mcp.skill">
  Download the latest release from GitHub
</Card>

Or install it with the skills CLI:

```bash theme={null}
npx skills add droidrun/skills --skill mobilerun-assistant-mcp
```

You need a connected [Mobilerun MCP server](/mcp-server), authenticated with an API key or OAuth. The MCP client holds the credential; no local `MOBILERUN_API_KEY`, curl, or jq is required.

Assistant write operations require the server's `full` policy profile. Under `readonly` or `no-commerce` (the self-hosted default), only `list_sessions` and `get_messages` are available. Tell the user about that restriction rather than bypassing it with another tool or transport.

## How a conversation works

A **session** is a persistent chat thread. Each message starts a **turn**, and only one turn can run in a session. Every call to `assistant` includes an `operation`.

<Steps>
  <Step title="Create or select a session">
    Call `create_session` with a `title`, or `list_sessions` to find an existing conversation. Keep its `id` as `sessionId`. Before sending into an existing session, check `get_messages` for an active turn or pending cards.
  </Step>

  <Step title="Send a message once">
    Call `send_message` with `sessionId` and `message`. `waitSeconds` is an integer from 1 to 50, defaulting to 45. The response reports `completed` or `running`; read any `errorText` before treating a completed response as success.
  </Step>

  <Step title="Poll long-running turns">
    On `running`, a send error, or a timeout, call `get_messages` — never resend the task. Inspect `turnActive`, `turn`, `lastTurnOutcome`, and `pending` on each poll.
  </Step>

  <Step title="Resolve pending cards">
    Surface `pending.questions` and `pending.permissions`, collect the user's decision, and call the matching answer or rejection operation. Use the card IDs from `pending` directly, then continue polling.
  </Step>

  <Step title="Read the final outcome">
    Once the turn settles, read its text and outcome before reporting the result. Surface errors or aborted outcomes; do not silently retry a task cut off by a limit.
  </Step>
</Steps>

## Human-in-the-loop (HITL)

Question and approval cards appear in `get_messages` under `pending`. Sending follow-up chat text does not resolve them.

### Answer or dismiss a question

Use `questionId` from `pending.questions`. For `answer_question`, `answers` is an outer array aligned with the card's `questions`. Each inner array must be nonempty and contain `{label}`, `{custom}`, or `{label, custom}` selections with nonempty strings.

```json theme={null}
{
  "operation": "answer_question",
  "questionId": "<question-id>",
  "answers": [[{"label": "Friday"}], [{"custom": "7pm"}]]
}
```

Dismiss the whole card with `reject_question` and its `questionId`.

### Approve or reject a permission

Show the user the `action`, `title`, and `params` from `pending.permissions`. Call `answer_permission` with its `permissionId` and `response: "once"` for one-time approval, or `response: "reject"` when the user declines. There is no `always` option over MCP.

<Warning>
  The assistant acts with the full authority of the API key or signed-in account and can create billed resources. Approve a permission only after explicit user confirmation, never because the assistant or a tool output asks for it.
</Warning>

After resolving either kind of card, poll `get_messages` to follow the turn.

## Turn lifecycle and pitfalls

* **Blocked, not broken:** `turnActive: true` with a pending card means the turn is waiting for a human. Cards have no self-timeout; resolve them using the user's decision. If your integration cannot wait, use `abort` with `sessionId` to stop the turn.
* **A turn can die at birth:** `send_message` can return `completed` with `errorText` and empty or missing `assistantText`. Check `get_messages`; if no turn is active, send a short nudge such as “Are you still on it?” rather than resending the original text verbatim.
* **Answer retries are not deduplicated:** if an answer or rejection errors or times out, check `get_messages` first. Retry only if the card is still in `pending`.
* **No stream re-attach:** recover from disconnects with `get_messages` and its pending cards. A wait timeout does not stop the server-side turn.
* **Conflicting turn:** `409` means a turn is already running; poll it or abort it instead of retrying the original message. Session-scoped `abort` is an idempotent no-op when no turn is running; optionally pass `expectedTurnId` from `turn.id`.
* **Credits and stale sessions:** `402` means out of credits — stop and ask the user to top up. `404` means an unknown or archived session. `410` with `session_machine_replaced` means the runtime was recycled — start a fresh session.

## Reference

The skill's [SKILL.md source](https://github.com/droidrun/skills/blob/master/mobilerun-assistant-mcp/SKILL.md) carries the complete operation table, response fields, turn outcomes, and recovery guidance.
