Skip to content
Blog

How to connect and use Ramus from Windsurf

Sign in to Ramus from Windsurf, now Devin Desktop, then let its agent install, check and share your Android app.

WindsurfPublished

On this page

This guide is for developers who build an Android app with Windsurf's agents. Windsurf has been called Devin Desktop since June 2026. New conversations use the Devin Local agent, and Cascade is still there as the legacy agent. Both use the setup below. 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.

What you need

  • Devin Desktop, the current release of Windsurf, which Windsurf installs as an update.
  • 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. The agent has a terminal, so it can drive Ramus with the ramus CLI once you run ramus login. For Ramus's MCP tools, this guide uses an API key. Devin Desktop can sign in to MCP servers with OAuth, but we have not confirmed that its sign-in completes with Ramus, and a key works the same for Devin Local and Cascade.

Sign in with the CLI

The 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. Devin Desktop loads project skills from .devin/skills/:

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

The agent uses it when a task involves an Android app, and /android-use calls it by name. For a lighter setup, add one line to AGENTS.md, which Devin Desktop reads as an always-on rule:

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

Rules in .devin/rules/, .windsurf/rules/ and .windsurfrules work too. If you turned on Devin Local's sandbox, allow *.ramus.dev and the storage host in the APK upload URL in its network rules, and let ramus write to ~/.ramus.

With both set up, tell the agent which to use. MCP gives the agent typed tools and returns screenshots straight to the model. The CLI is quicker with a local APK.

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. Devin Desktop 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 windsurf 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. On macOS the agent's terminal is zsh and reads ~/.zshrc:

~/.zshrc
export RAMUS_API_KEY="rms_your_key_here"

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

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

Devin Desktop replaces ${env:RAMUS_API_KEY} with the variable's value when it connects. This is the user-level file: Devin Local reads it, and Cascade's Open MCP config file action opens it. On Windows it is %APPDATA%\devin\mcp_config.json.

To share the setup with your team, put the same entry in .devin/mcp_config.json in your project and commit it. It holds no secret, and each person sets their own key. .devin/mcp_config.local.json is the place for anything personal, and git ignores it.

Older Windsurf builds read ~/.codeium/windsurf/mcp_config.json, where the URL key is serverUrl. Devin Local imports servers from that file too, so a Ramus entry you already have there keeps working.

Check that it connected

In the Agents window, choose Customize, then MCPs, and set Install MCPs for to This computer. ramus should be listed and switched on. Use the refresh action after you edit the file. In Cascade, open the MCP menu and press refresh. Each server writes its log to its own MCP: ramus output channel.

If you installed the Devin CLI (Install Devin CLI in Devin Desktop), the same check works in a terminal:

bash
devin mcp list

After the agent's first Ramus call, open the Agents page. Your windsurf key appears under Connected, with its last call and the devices it started in the last 7 days. Revoke there cuts it 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

Devin Local asks before every MCP tool call. When it asks, you can allow that tool, or every tool on ramus, for the session or permanently. Keep the prompt for starting and ending devices, which use your minutes. To approve the tools that only read the device ahead of time, add them to your Devin Local permissions:

~/.config/devin/config.json
{
	"permissions": {
		"allow": [
			"mcp__ramus__android_snapshot",
			"mcp__ramus__android_screenshot",
			"mcp__ramus__android_wait_for",
			"mcp__ramus__android_logcat",
			"mcp__ramus__android_session_status",
			"Exec(ramus)"
		]
	}
}

mcp__ramus__* allows every Ramus tool. Exec(ramus) lets the agent run the ramus CLI without asking. You can add the same rules in Devin Local settings under Allow. The session mode decides the rest: Code asks, Smart lets a fast model approve commands it judges safe, and Bypass Permissions runs everything your organization allows.

From a Devin cloud session

Devin Desktop can also hand work to Devin in the cloud. A cloud session cannot read the key in your shell. Create a separate key on the Agents page, such as devin-cloud, and add it to Devin's secrets as RAMUS_API_KEY. Devin binds a secret to the commands that need it, so keep the AGENTS.md line: the agent reference tells it that the ramus CLI reads RAMUS_API_KEY.

Start a device

YoutoDevin Local

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.

YoutoDevin Local

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.

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

YoutoDevin Local

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 the agent 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:

YoutoDevin Local

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:

YoutoDevin Local

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 Needs auth or an error. Ramus did not get a valid key, and its 401 answer includes a sign-in challenge. Check that RAMUS_API_KEY is set where Devin Desktop runs: quit it, then start it from a terminal that has the variable, with devin-desktop .. Check on the Agents page that the key has not expired or been revoked.
  • ramus is missing from the MCP list. Check the file path and that the JSON is valid, then refresh the list. A Windsurf-era entry in ~/.codeium/windsurf/mcp_config.json uses serverUrl, not url.
  • Your team blocks the server. On Teams and Enterprise plans, an admin's MCP allowlist or registry can block servers that are not on it. Ask the admin to allow https://api.ramus.dev/api/mcp.
  • ramus commands keep asking for approval. Add Exec(ramus) to your allow rules, or allow it permanently from the approval card.
  • The agent ignores the Ramus tools. Switch off MCP servers 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.