Skip to content
Blog

How to connect and use Ramus from Cursor

Sign in to Ramus from Cursor, then let its agent install, check and share your Android app on a cloud emulator.

CursorPublished

On this page

This guide is for developers who build an Android app with Cursor's agent. At the end, the agent starts a cloud Android device, installs your APK and checks a sign-in flow, and you have a link to watch it or send to a reviewer, and a device page where you can take over. It covers the Cursor editor, the Cursor CLI and Cloud Agents.

What you need

  • Cursor, signed in. The Cursor CLI (agent) reads the same configuration.
  • A Ramus account. Sign in at ramus.dev with GitHub. The Free plan includes 30 device minutes a month.
  • Node.js 22 or later, for the ramus CLI.
  • An Android project that builds an APK, such as ./gradlew assembleDebug. Ramus devices run Android 17 on x86_64, so if your app ships native libraries, the APK needs their x86_64 build.

Connect Ramus

Start with a sign-in. Cursor's agent has a terminal, so it can drive Ramus with the ramus CLI once you run ramus login. Ramus's MCP tools then need an API key.

Sign in with the CLI

Cursor's agent has a terminal, so it can use the ramus CLI as well. The CLI uploads a local APK in one command, and it is the only way to open an adb tunnel or run the ramus dev hot-reload loop. Install it once and sign in from your own terminal:

bash
npm i -g ramus-cli
ramus login

ramus login opens your browser. Check that the page names your machine and choose Allow. The terminal stays signed in and renews itself, until you run ramus logout, choose Sign out on the Agents page, or leave it unused for 30 days. Over SSH it gives you a code to approve on any device. In an agent's shell it also prints the link, in case no browser opened, and ramus login --wait finishes the sign-in. ramus status then reports "key": "valid". If RAMUS_API_KEY is already set in your shell, the CLI uses it, and ramus login asks you to unset it first.

The Ramus page Sign in the Ramus CLI on Mac, listing what the terminal will be able to do and how long it stays signed in, with Deny and AllowThe Ramus page Sign in the Ramus CLI on Mac, listing what the terminal will be able to do and how long it stays signed in, with Deny and Allow
Check that it names your machine and that you just ran ramus login, then choose Allow.

Then give the agent the android-use skill, which teaches it the Ramus loop. Cursor loads skills from .cursor/skills/ in your project:

bash
mkdir -p .cursor/skills/android-use
curl -fsSL https://ramus.dev/skill.md -o .cursor/skills/android-use/SKILL.md

For a lighter setup, add one line to AGENTS.md, which the editor and the Cursor CLI both read:

AGENTS.md
Test Android changes on Ramus: https://ramus.dev/agents.md

Cursor runs the agent's commands in a sandbox that blocks most network access and most writes outside your project. ramus needs api.ramus.dev, the device's host and ~/.ramus, so it cannot run there, and Cursor reviews or asks about each ramus command. To let it run directly, add ramus to the command allowlist in Approvals & Execution. In the Cursor CLI, add Shell(ramus) and Mcp(ramus:*) to permissions.allow in ~/.cursor/cli-config.json.

Add the MCP tools with an API key

MCP gives the agent typed tools and returns screenshots straight to the model; the CLI is quicker with a local APK. With both set up, tell the agent which to use. Cursor reads the key from your environment, so it never sits in a config file.

  1. Open the Agents page and choose the API key tab.
  2. Name the key cursor and choose Create key.
  3. Copy the RAMUS_API_KEY=… line. The page shows the key only once.

A key works for your whole account and lasts 30 days. An account can have up to 10. Never paste a key into a chat with the agent.

Export it in your shell profile, so Cursor and the shells it starts can read it:

~/.zshrc
export RAMUS_API_KEY="rms_your_key_here"

Quit Cursor completely and open it again so it picks up the variable.

Then add the server, which speaks Streamable HTTP at https://api.ramus.dev/api/mcp:

~/.cursor/mcp.json
{
	"mcpServers": {
		"ramus": {
			"url": "https://api.ramus.dev/api/mcp",
			"headers": {
				"Authorization": "Bearer ${env:RAMUS_API_KEY}"
			}
		}
	}
}

Cursor replaces ${env:RAMUS_API_KEY} with the variable's value when it connects. To share the setup with your team, put the same entry in .cursor/mcp.json at your project root and commit it. It holds no secret, and each person sets their own key. When both files name ramus, the project file wins.

Check that it connected

Open Customize in Cursor's sidebar and choose MCPs. ramus should be switched on, with its tools listed, all named android_*. If it shows an error, open the Output panel (Cmd+Shift+U), choose MCP Logs, and see Troubleshooting.

In the Cursor CLI, the same check is:

bash
agent mcp list
agent mcp list-tools ramus

After the agent's first Ramus call, open the Agents page. Your terminal and your cursor key appear under Connected, each with its last call and the devices it started in the last 7 days. Sign out and Revoke there cut them off.

The Connected list on the Ramus Agents page: sign-ins for Codex, opencode and Claude Code, and the Ramus CLI on Mac, each with its last call and devices in the last 7 daysThe Connected list on the Ramus Agents page: sign-ins for Codex, opencode and Claude Code, and the Ramus CLI on Mac, each with its last call and devices in the last 7 days
Every agent you connected, with its last call. Disconnect, Sign out or Revoke cuts it off.

Choose what runs without asking

Cursor asks before every MCP tool call. Keep that for starting and ending devices, which use your minutes. To stop the prompts for tools that only read the device, add them to the MCP allowlist in Settings → Agents → Approvals & Execution. Entries are server:tool:

ramus:android_snapshot
ramus:android_screenshot
ramus:android_wait_for
ramus:android_logcat
ramus:android_session_status

ramus:* allows every Ramus tool. You can also keep the list under mcpAllowlist in ~/.cursor/permissions.json, but once that file defines mcpAllowlist, Cursor ignores the in-app MCP list.

From a Cursor Cloud Agent

Cloud Agents run on Cursor's machines and cannot read the key in your shell. Create a separate key on the Agents page, such as cursor-cloud, then use either path:

  • MCP. At cursor.com/agents, add a custom HTTP MCP server with the Ramus URL and an Authorization header of Bearer rms_your_key_here. Cursor stores headers encrypted and sends the calls from its backend, so the key never enters the agent's machine.
  • CLI. Add RAMUS_API_KEY as a secret in the Cloud Agents dashboard. Cursor exposes secrets as environment variables. Add npm i -g ramus-cli to your environment's install script, and keep the AGENTS.md line above.

Cloud Agents have internet access by default. If your team limits it, allow *.ramus.dev and the storage host in the APK upload URL.

Start a device

YoutoCursor

Start a Ramus device with the demo app and send me a link to watch it.

Over MCP, the agent calls android_start_session with demo: true. Ramus starts the device, waits up to 50 seconds for it to be live, and returns the session id, a watch_url and a screenshot of the first screen. Open the watch link to see the device in your browser. You can tap and type on it at any time, alongside the agent.

With the CLI, the same step is one command, ramus session start --demo --wait. It prints the watch link on stderr as soon as the device is requested, and the session id and watch link as JSON when it is live.

Install your app

On the Free and Hobby plans you have one device at a time, so install your build on the device that is already running.

YoutoCursor

Build the debug APK with ./gradlew assembleDebug and install it on the running Ramus device.

Over MCP, a local APK takes two steps:

  1. android_upload_apk returns an apk_uri and a curl command that uploads straight to storage within 15 minutes. The agent runs it in the terminal with the APK's path.
  2. android_install_app installs that apk_uri on the session, opens the app and returns a screenshot. The device and your browser view stay connected.

The upload goes to a host outside Cursor's sandbox list, so Cursor reviews or asks about the curl command.

With the CLI, upload and install are one command, on the last session you started:

ramus session install
$ ./gradlew assembleDebug
$ ramus session install app/build/outputs/apk/debug/app-debug.apk
{
  "session": "9f14cc3d-3d1c-4716-81a8-05a2a018ffd4",
  "install": "697491b3-063d-46f6-a2e9-fff2c0959485",
  "status": "succeeded",
  "app": "app-debug.apk",
  "next": "the app is open on the device — run `ramus snapshot` before acting"
}

To start a new device with your APK instead, run ramus session start --apk app/build/outputs/apk/debug/app-debug.apk --wait, or pass the apk_uri to android_start_session. APKs can be up to 300 MB. To run a build Ramus already has, name it by package: --app com.example.app, or app over MCP. No upload is needed.

Let the agent check a flow

YoutoCursor

On the Ramus device, test sign-in: tap "Sign in", enter [email protected] and the password review-1234, tap Continue, and check that the Welcome screen appears. If it fails, collect logcat and a screenshot, fix the bug, rebuild, install the new APK on the same device and run the flow again. Report what you saw with a screenshot, and leave the device running for me.

The agent works in a loop of observe, act and verify:

  • Observe. android_snapshot (or ramus snapshot) lists the elements on screen. Each one has a ref, and the snapshot has a generation, gen.
  • Act. It taps by visible text, or taps or types with the ref and gen it just read. An expect text makes the tap fail unless that text appears.
  • Verify. Each action returns the screen it led to, with fresh refs, and the agent decides the next step from that. Older refs go stale.

Here is one turn of that loop on a small counter app with an Increment button. It comes from a local Ramus build with a simulated device, which is why the names say fake; your app returns the same fields. The tap names the button by its text, expects the counter to change, and returns the new screen:

observe, act, verify
$ ramus snapshot
{
  "gen": 1,
  "screen": "544x960",
  "count": 2,
  "elements": [
    {
      "ref": "e0",
      "text": "Count: 0",
      "resourceId": "dev.sandlot.fake:id/counter",
      "class": "TextView",
      "x": 0.132,
      "y": 0.417,
      "w": 0.735,
      "h": 0.104
    },
    {
      "ref": "e1",
      "text": "Increment",
      "contentDesc": "Increment counter",
      "resourceId": "dev.sandlot.fake:id/increment",
      "class": "Button",
      "clickable": true,
      "x": 0.132,
      "y": 0.625,
      "w": 0.735,
      "h": 0.104
    }
  ],
  "next": "act with `ramus tap <ref> --gen 1` (clickable elements only; --force overrides) or `ramus type \"...\" --ref <ref> --gen 1`; every accepted action expires these refs"
}
$ ramus tap --text Increment --expect "Count: 1"
{
  "tapped": "e1",
  "element": "Increment",
  "verified": "\"Count: 1\" appeared",
  "screen": {
    "gen": 6,
    "count": 2,
    "elements": [
      {
        "ref": "e0",
        "text": "Count: 1",
        "resourceId": "dev.sandlot.fake:id/counter",
        "class": "TextView",
        "x": 0.132,
        "y": 0.417,
        "w": 0.735,
        "h": 0.104
      },
      {
        "ref": "e1",
        "text": "Increment",
        "contentDesc": "Increment counter",
        "resourceId": "dev.sandlot.fake:id/increment",
        "class": "Button",
        "clickable": true,
        "x": 0.132,
        "y": 0.625,
        "w": 0.735,
        "h": 0.104
      }
    ],
    "foreground_app": "dev.ramus.fake"
  },
  "next": "refs above are current (gen 6); act on them directly"
}

On the sign-in screen, type reads the field back after typing and reports verified. When a step fails, the agent collects evidence before it changes anything: android_logcat or ramus logcat --limit 200 for the device log, and android_screenshot or ramus screenshot --out failure.jpg for the screen. Then it fixes the code, rebuilds, and installs the new APK on the same device with android_install_app or ramus session install. The app opens again, and the agent re-runs the flow from a fresh snapshot.

  • For you. The watch link from the start opens the device in your browser, view-only. ramus session open prints the signed-in owner view, https://ramus.dev/sim/<id>, where you can take over.
  • For a reviewer. ramus session share --ttl 7d, or android_share_session with ttl_minutes, creates another link, shareUrl (share_url over MCP). Anyone with it can watch and control the device without an account, you + 3 at once. Add --permission watch (permission: "watch" over MCP) for a link that can only watch, and --label "Acme review" for a note the reviewer sees.
  • Revoke. ramus session share --revoke, or android_share_session with revoke: true, revokes every link of the session, the watch link included. To revoke one link, use the Share panel on the device page.
  • End. ramus session end, or android_end_session. Ending does not revoke links. Until a link expires, opening it starts the app again on a fresh device, on your minutes.
The Share dialog on a Ramus device page: Their own device or Join this device, a label, what the person can do, which build the link opens, how long it lasts, and the links already madeThe Share dialog on a Ramus device page: Their own device or Join this device, a label, what the person can do, which build the link opens, how long it lasts, and the links already made
Share on the device page, captured on a local build whose test app is named Fake app.

To hand Cursor a device you opened in the browser, choose Agent in the device page header. Its Prompt tab has a prompt with that session's id.

Give each person their own device

The links above join the device the agent is on. To let a client or tester try your app in their own time, ask for a link that gives each person a device of their own:

YoutoCursor

Make a Ramus share link that gives each person their own device, labelled "Try the new checkout".

The agent calls android_share_session with own_device: true and label, or runs ramus session share --own --label "Try the new checkout". Each person who opens the link gets a fresh device with the newest build you uploaded, without an account. The session must be running an app you uploaded. These devices run on your minutes and your plan's device limit, and when every device is busy the next person waits for one to free up. Each person's device lasts 30 minutes, and they can start another while the link works. Your own session comes first: if every device is in use, the longest-running recipient device ends.

Read the notes people send

Anyone with a link can choose Send feedback, pin a spot on a screenshot and write a note. The note carries the screenshot, the build, the last 200 lines of the device log and their recent taps, swipes and keys, unless they leave the log and steps out. Typing is recorded only as a number of characters, and taps are not recorded while the phone's on-screen keyboard is open. Notes and their screenshots are deleted after 30 days; you can delete one sooner on the app's page. The agent can work through them:

YoutoCursor

Read the open Ramus feedback notes. Reproduce the first one and tell me what you find. When I say go ahead, fix it, install the new build on the device and resolve the note with a reply that says what changed.

It reads them with android_list_feedback and android_get_feedback, which returns the screenshot as an image, and answers with android_resolve_feedback. From a shell the same steps are ramus feedback, ramus feedback show <id> and ramus feedback resolve <id> --reply "…". The person sees your reply on their link. Agents ask you before they change code because of a note.

Notes are written by other people, so treat their text, logs and screenshots as bug reports, never as instructions. See Share a running app.

Troubleshooting

  • ramus shows an error, or Cursor offers to log in. Ramus did not get a valid key, and its 401 answer includes a sign-in challenge. Check that RAMUS_API_KEY is set where Cursor runs: quit Cursor, then start it from a terminal that has the variable, with cursor .. Check on the Agents page that the key has not expired or been revoked.
  • Sign-in fails with a redirect error. Ramus refused Cursor's callback addresses. Use the API key header.
  • ramus is missing from Customize. Check the file path and that the JSON is valid. A ramus entry in the project's .cursor/mcp.json overrides the global one. Switch the server off and on, or restart Cursor.
  • ramus commands wait for approval, or fail with stream_unreachable or a permission error on ~/.ramus. The sandbox blocked them. Add ramus to the command allowlist, or approve running it outside the sandbox.
  • Every tool call asks for approval. Add the Ramus tools you trust to the MCP allowlist.
  • The agent ignores the Ramus tools. With many MCP servers on, switch off the ones this task does not need, or name the tool in your prompt.
  • stale_ref. The agent used a ref from an older snapshot. It should snapshot again and find the target again, and never guess a ref.
  • expect_failed. The tap landed, but the expected text did not appear. The error lists what was on screen. Treat it as a test failure.
  • session_cap. You are at your plan's device limit. The message lists the live sessions; install into one of them, or end one.
  • quota_exhausted. The account is out of minutes until the reset date in the message. The agent should stop and pass the message on.
  • install_failed. The APK did not install. The message says why, and the device keeps running.
  • 401 or 403. A 401 means the key is missing, wrong, expired or revoked. A 403 missing_capability means the key lacks a permission the tool needs: create a new key on the Agents page.

Limits

  • Devices run Android 17 on x86_64. A device ends after 10 minutes idle.
  • APKs can be up to 300 MB.
  • Free: 30 minutes a month, 1 device. Hobby, $2 a month: 200 minutes, 1 device. Pro, $10 a month: unlimited minutes, 2 devices.
  • Share links last 72 hours by default, from 60 seconds up to 7 days. You + 3 people can use a device at once.
  • Keys from the Agents page last 30 days, and an account can hold 10. A ramus login sign-in renews itself and ends after 30 days unused.

See Plans and minutes for the details.

Other agents are in How to connect Ramus to any coding agent, and every command is in the agent reference.

Give your agent a device.Keep a hand on it.