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
ramusCLI. - 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:
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.


Then give the agent the android-use skill, which teaches it the Ramus loop.
Devin Desktop loads project skills from .devin/skills/:
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:
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.
- Open the Agents page and choose the API key tab.
- Name the key
windsurfand choose Create key. - 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:
export RAMUS_API_KEY="rms_your_key_here"
Quit Devin Desktop completely and open it again so it picks up the variable.
{
"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:
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.


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:
{
"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
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.
Build the debug APK with ./gradlew assembleDebug and install it on the running Ramus device.
Over MCP, a local APK takes two steps:
android_upload_apkreturns anapk_uriand acurlcommand that uploads straight to storage within 15 minutes. The agent runs it in the terminal with the APK's path.android_install_appinstalls thatapk_urion 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:
$ ./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
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(orramus snapshot) lists the elements on screen. Each one has aref, and the snapshot has a generation,gen. - Act. It taps by visible text, or taps or types with the
refandgenit just read. Anexpecttext 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:
$ 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.
Share a link
- For you. The watch link from the start opens the device in your
browser, view-only.
ramus session openprints the signed-in owner view,https://ramus.dev/sim/<id>, where you can take over. - For a reviewer.
ramus session share --ttl 7d, orandroid_share_sessionwithttl_minutes, creates another link,shareUrl(share_urlover 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, orandroid_share_sessionwithrevoke: 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, orandroid_end_session. Ending does not revoke links. Until a link expires, opening it starts the app again on a fresh device, on your minutes.


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:
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:
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
ramusshows Needs auth or an error. Ramus did not get a valid key, and its 401 answer includes a sign-in challenge. Check thatRAMUS_API_KEYis set where Devin Desktop runs: quit it, then start it from a terminal that has the variable, withdevin-desktop .. Check on the Agents page that the key has not expired or been revoked.ramusis 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.jsonusesserverUrl, noturl.- 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. ramuscommands keep asking for approval. AddExec(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_capabilitymeans 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 loginsign-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.