Remote Harness MCP
Supports Android devices running the node under Termux, exposing device-level tools for battery, network, sensor, camera, and microphone information, plus device control, screen capture, and message access.
Connects Google Gemini (Spark web and mobile app) to remote machines over MCP, letting Gemini run shell commands, manage files and git, start and check jobs, monitor system resources, and control connected PCs or servers from a chat.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
π± Remote Harness MCP β control any PC from Gemini (App & Web)
An MCP server that turns Google Gemini from a chatbot into a full-fledged mobile agent. Type a task in the Gemini app on your phone β your PC, laptop or server executes it, and Gemini reports back with real results.
English | π·πΊ Π ΡΡΡΠΊΠ°Ρ Π²Π΅ΡΡΠΈΡ
π‘ The idea
Gemini in a browser or on a phone can talk, but it cannot do anything on your computer. Remote Harness MCP closes that gap. It is a free, open-source Model Context Protocol (MCP) server that you connect to Gemini Spark (gemini.google.com/customize/connectors or the Gemini mobile app) with a single link. After that, Gemini gets hands:
runs shell commands, builds and tests code, manages git;
reads, writes and edits files;
searches the file system;
starts long-running jobs and checks on them later;
watches CPU, RAM and disk.
π± Gemini app / π gemini.google.com (Spark)
β MCP (SSE / Streamable HTTP)
βΌ
βοΈ Gateway racknerd-5a24bf9.merino-carob.ts.net β relay only, TLS
β outbound WebSocket tunnel (no open ports)
βΌ
π» Your PC Β· laptop Β· VPS Β· home server β executes the taskResult: your phone becomes a remote control for every machine you own β and Gemini is the agent that operates them.
Related MCP server: TerminalMCP
π₯ What you can do with it
Just ask Gemini in plain language, from anywhere:
"Check why my home server is slow and show the top processes"
"Pull the latest changes in ~/projects/api, run the tests and tell me what failed"
"Fix the typo in config.yaml and restart the service"
"Start a full backup in the background and tell me when it's done"
"How much free disk space is left on my laptop?"
"Turn on the phone's torch, and tell me its battery level and signal"
No SSH client, no terminal on your phone, no VPN β only a chat.
πΈ In action
Three exchanges from one chat, each answered by a different machine on the other side of the bridge. The phone never opened a terminal.
The laptop describes itself β "@Rem Matebook-deb, give me the system and hardware specs":

The phone reports its own battery, radio and VPN β "@Rem Phone, check".
The SSID and both addresses are mosaicked; everything else came from device_info:

The desktop is photographed, published and read back β "take a screenshot of @RemMatebook-deb", then "describe what is running on the screens":

Every answer was produced by system_vitals, device_info, device_capture and
share_file running on the node itself β the model only ever saw tool results.
β¨ Why it works this way
π± Mobile-first agent | Works in the official Gemini mobile app and on the web through Gemini Spark. |
π₯οΈ Any PC | Linux, macOS, Windows β desktops, laptops, VPS, containers. Android phones and tablets run the same node inside Termux. |
π Behind any NAT | The node opens an outbound WebSocket tunnel. No public IP, no port forwarding, no router setup. |
β‘ One-line install | Detects OS, architecture and device type; installs autostart ( |
π‘οΈ Two credentials, no secret in a URL | The device token opens the tunnel and travels only in an |
π Many machines, one gateway | Each node is addressed by |
π Free & open source | MIT license, works with the free Gemini tier. |
π οΈ What Gemini can do on your machine (MCP tools)
26 tools. Every one of them runs on your machine under your own user account β the gateway only carries the calls.
Which calls ask for your confirmation
Gemini Spark decides whether to stop and ask "confirm this action?" from the MCP annotation hints each tool advertises (readOnlyHint, destructiveHint, idempotentHint, openWorldHint). A tool advertised with no hints at all is treated as destructive by the spec's defaults β that is why a server that declares none gets a confirmation prompt on every single call. Here the hints are explicit:
Class | Tools | Confirmation |
Read-only |
| never asked |
Local work β commands, builds, file writes, jobs, shares, and the whole device branch |
| not asked |
Confirmed β install/delete, |
| asked first |
A prompt is spent on a system change only: system_change, system_write and mesh_update install, remove, delete or replace something you cannot undo in one call. Everything else runs silently, the device branch included β a camera, a microphone or an SMS list installs and removes nothing, and what still refuses such a call is the node's own switch (MESH_DEVICE, MESH_DEVICE_ACTIONS, MESH_DEVICE_PIM, MESH_READ_ONLY), which holds no matter what the client does. unshare is ordinary local work for the same reason: it stops a local server and removes a copy you can publish again in one call.
The mechanics matter if you also run the node in standalone mode: Gemini Spark skips its dialog only for a tool it sees as readOnlyHint: true, so the gateway advertises every non-system call that way β the shell and file twins because its classifier refuses the gated command classes, the device branch because the node refuses the call before any termux-* command runs. The node's own tools/list keeps the literal hint instead (a camera is not a read), which is why a client connected straight to core/server.py may still ask.
Four classes are gated, and the gateway β not the hint β enforces them, because a hint is static per tool and cannot tell ls from apt install:
Installing or removing software β package managers, uninstallers,
msiexec,dpkg/rpm,curl β¦ | bash,iwr β¦ | iex.Deleting data β
rm,Remove-Item,del,rd,shred,dd,mkfs,git clean -fd,docker rm/prune,truncate -s 0.Running as root β
sudo,doas,pkexec,runas,Start-Process -Verb RunAs.System paths β
/etc,/usr,/boot,/var/lib,/opt,/dev,C:\Windows,C:\Program Files,ProgramData, the registry. Reads of those paths (cat /etc/os-release,systemctl status) are not gated, and neither is ordinary work in your own directories.
bash_exec/run_job refuse such commands and tell the model to re-issue them as system_change; write_file/edit_file refuse a system path and point at system_write (which takes the exact content, so there is no shell quoting to get wrong). The gate is moved, not removed β inspection, builds, tests, config edits in your projects and the like still run without a dialog.
The tool surface and the confirmation policy live in gateway.py (annotations, INSTALL_COMMAND_PATTERNS / DELETE_COMMAND_PATTERNS, SYSTEM_PATH_RE, classify_command()) and in core/mcp_tools.py (TOOL_ANNOTATIONS for the node's own surface, which also carries the device branch's four tools); tests compare the two surfaces and pin the classifier, so a gated command cannot slip through.
The classifier matches a verb at the start of a command segment (aftersudo/env/timeout/powershell -Command prefixes), so echo "rm -rf /" or grep rm notes.txt still run, while curl β¦ | bash counts as an install. It is a UX gate, not a sandbox: for hard guarantees independent of any prompt use the node's own switches β MESH_READ_ONLY=1 and MESH_WRITE_ROOTS (core/agent.py).
Status and host information
Tool | What it does |
| Confirms the node is reachable, with live evidence from the host itself: what kind of machine answered ( |
| One-call host summary: OS, desktop, user, home, disks, memory, load, top processes, the current wallpaper, |
| CPU, RAM and disk metrics, plus the battery block and the thermal sensors the platform exposes. |
Shell
Tool | What it does |
| Runs a shell command. The shell matches the host, not the tool's name β |
| Installs, removes or deletes on the host, runs privileged commands and edits system paths β the confirmed twin of |
Files
Tool | What it does |
| Lists files and directories (workspace by default). |
| Reads a text file with line numbers, optionally restricted to a line range. Paginated like |
| Atomically creates or overwrites a file (temp file + |
| Replaces an exact substring. |
| Writes a file inside a system path ( |
Search
Tool | What it does |
| Recursively searches file contents, skipping binary files and heavy directories. |
| Finds files by glob pattern ( |
Long-running work
Tool | What it does |
| Starts a command in the background and returns a |
| Reads a job's output, optionally waiting up to 20 s for completion. Paginated. |
| Terminates a job. |
| Lists recent jobs, newest first (20 by default, max 50), including whether each one holds a |
Device (phone, laptop or server)
These four tools read and act on the device the node itself runs on. They are advertised on every node β a phone-only action on a laptop answers with available: false, the reason and, where one exists, the fix, instead of failing.
Tool | What it does |
| One-call device report: battery and charging, network interfaces with Wi-Fi and cellular signal, language, time and timezone, CPU, RAM, storage, cameras, microphones, sensors and what the platform can actually reach. |
| Twenty reversible actions: |
| Eleven actions: |
| Five actions: |
Example:
"Check the phone's battery and signal."
β device_info(section="summary")
"Turn on the torch."
β device_control(action="torch", on=true)
"Take a photo and share it with me."
β device_capture(action="camera_photo") # runs without a dialog
β share_file(path=<the path it returned>)Device switches (operator)
Read from agent.env at startup (core/agent.py, applied in core/mcp_tools.py and core/device.py):
Variable | Default | What it does |
|
|
|
| empty | Comma-separated allowlist of action names for |
| shared storage on a phone once | Where photos and recordings are written. |
|
|
|
|
|
|
|
|
|
Publishing files and web pages
These four make something on your machine readable from the internet. The link is served by the gateway on the shared domain, so the node has to stay connected for it to work.
Tool | What it does |
| Publishes one file: it is copied into the node's share root and you get |
| Serves a directory in place β no copy is made. |
| Lists active shares with their URLs, their roots, and whether the local server is running. |
| Stops a share and revokes its link. Accepts the share name, its slug or the full URL. A file share also deletes the copy in the share root; a served directory is left untouched on disk. |
The random part of the pathis the credential β anyone who has the link can read the file or browse the directory. Treat a share link like a password, and unshare it when you are done. Size limits: 32 MiB per file by default, 64 MiB ceiling via MESH_WEB_MAX_BYTES.
Agent behaviour
Tool | What it does |
| Loads the agent's current operating rules and orchestration skill. |
Keeping the node up to date
Tool | What it does |
| Reports, checks for or installs a newer release of the node's own code. |
Nodes update themselves: a running agent checks for a newer release in the background (every 6 hours by default) and installs what it finds, and on Windows theRemoteHarnessMCPUpdater task checks once a day even when no agent is running. MESH_UPDATE_AUTO=0 switches that to "report only". Details, environment variables and rollback: docs/UPDATES.md.
Gemini gives a single tool call roughly 30 seconds. For anything longer (builds, backups, downloads) the agent usesrun_job and polls job_output, so tasks never get cut off.
π Quick start β 2 minutes
1. Install the node on the PC you want to control
π§ Linux / π macOS
curl -fsSL https://racknerd-5a24bf9.merino-carob.ts.net/install.sh | bashπ± Android (Termux) β no root, works from F-Droid's Termux
curl -fsSL https://racknerd-5a24bf9.merino-carob.ts.net/install.sh | bashAutostart on a phone is a runit service plus the Termux:Boot app, and the node is named after the device model β see docs/TERMUX.md.
πͺ Windows (PowerShell)
irm https://racknerd-5a24bf9.merino-carob.ts.net/install.ps1 | iexπͺ Windows β visual installer (from a clone or a release)
.\install-gui.cmdA WinForms wizard that mirrors the console installer's variants, switches language at runtime and shows the MCP link at the end β see docs/GUI_INSTALLER.md.
π¦ Node.js (any OS)
npx remote-harness-mcpThe pre-rename name still works: npx gemini-computer-use is an alias package
that depends on remote-harness-mcp and forwards every argument to the same CLI
(see npm/gemini-computer-use).
π οΈ From source
git clone https://github.com/LevRa7/remote-harness-mcp.git
cd remote-harness-mcp
./install.sh --quickAt the end the installer prints your personal MCP link and copies it to the clipboard:
https://racknerd-5a24bf9.merino-carob.ts.net/sse?user=<your-node-name>&token=<your_secret_token>2. Connect it to Gemini Spark
Open gemini.google.com/customize/connectors in a browser or in the Gemini mobile app.
Click Add connector (or Settings βοΈ β Connectors) and name it MyDevice.
Paste the link (Ctrl+V) into the Server URL field and click Connect.
3. Give it a task
"Show system_vitals and list the 5 biggest folders in my home directory."
That's it β Gemini is now an agent running on your machine. Repeat step 1 on other computers to control all of them from the same phone.
π₯οΈ Deployment modes
Mode | Command | When to use |
Gateway + tunnel (recommended) |
| Home PCs and laptops behind NAT β the mobile-agent scenario. |
Remote over SSH |
| Deploy a node onto a remote Linux server from your terminal. |
Local standalone |
| Pure localhost FastMCP server at |
On Windows the same variants are available in the visual installer (.\install-gui.cmd); local standalone stays console-only there (.\install.ps1 -Mode standalone -Port 8096).
On Android/Termux the same ./install.sh variants work; autostart is a runit service plus the Termux:Boot app instead of systemd, and standalone stays reachable from that phone alone β see docs/TERMUX.md.
πͺ Windows installation
Windows already ships PowerShell 5.1, so there is nothing to prepare β no Python, no Node.js, no administrator rights. Both installers finish by putting the same MCP link on the clipboard.
Console installer | Visual installer | |
Start |
|
|
Needs | nothing but PowerShell | a clone or an unpacked release β it drives |
Variants |
| Quick setup, Custom setup, Remote over SSH |
Language |
| switch in the window header, or |
Automatic dependencies and architecture
Nothing has to be prepared by hand first. Both installers obtain what they need, and if they cannot, they stop with an explicit message instead of writing an autostart entry that can never work.
Windows ( | Linux / macOS ( | |
Python |
|
|
|
|
|
If all of that fails | stops, and prints the exact command to run | stops, and prints the exact command to run |
websockets is the only external Python requirement: core/mcp_tools.py and
core/server.py are standard library only. uv is fetched only when the cheaper paths
have already failed.
The architecture decides which build is downloaded. The installer asks the OS, not
the process, because a 32-bit PowerShell on 64-bit Windows reports x86 and hides the
real value in PROCESSOR_ARCHITEW6432, and an emulated x64 process on ARM64 reports
AMD64:
Machine | Python build |
|
Windows x64 |
|
|
Windows ARM64 |
|
|
Windows 32-bit |
| β (uv ships no 32-bit Windows build) |
Linux | the package manager's own build |
|
Linux | the package manager's own build |
|
Linux | the package manager's own build |
|
macOS |
|
|
The interpreter that ends up pinned is the one that actually has websockets β which can
be a venv or a uv-managed CPython, both deliberately outside PATH. install.ps1 -DryRun
and install.sh --dry-run report the architecture without changing anything, and the visual
installer shows it in its preflight.
Download the setup executable
The release also ships a single compiled installer, for machines where you would rather
not clone or download anything else β RemoteHarnessMCP-Setup-<version>.exe:
.\RemoteHarnessMCP-Setup-0.4.8.exe # open the visual installer
.\RemoteHarnessMCP-Setup-0.4.8.exe -Lang ru # start in Russian
.\RemoteHarnessMCP-Setup-0.4.8.exe -SelfTest # headless self-check, prints JSON
.\RemoteHarnessMCP-Setup-0.4.8.exe --version # print the versionIt carries the wizard, install.ps1, core/ and install.sh inside itself, unpacks them
to %LOCALAPPDATA%\RemoteHarnessMCP\setup\<version> (override with MESH_SETUP_DIR) and
runs the wizard from there. It needs nothing but the .NET Framework and PowerShell that
Windows already has, and it is built from this repository by
.\build-installer-exe.ps1 β no SDK, no NuGet, no network.
It is not code-signed, so SmartScreen may ask for confirmation the first time you run it.
Visual installer

Quick setup (recommended) β node name = PC name, shared gateway domain, Windows autostart. The one to pick on a home PC or laptop.
Custom setup β your own node name, your own shared domain, optional token.
Remote over SSH β copy the node code to a Linux host and run
install.shthere. Needs the Windows OpenSSH Client feature and key-based authentication; interactive password prompts are not supported.
The first screen runs install.ps1 -DryRun and reports what it found β Python, websockets, the resolved gateway domain, the config directory, the autostart path β without changing anything. Before starting, the window shows the exact command it will run; during the run it streams the installer output; at the end it shows the MCP link with a copy button, the three-step Gemini instructions and the paths it created.
Full details, screenshots and the SSH notes: docs/GUI_INSTALLER.md.
What a Windows installation creates
Path | What it is |
|
|
|
|
| autostart entry, so the node comes back after a reboot |
| agent output, for when a silent autostart fails |
Language
The visual installer starts in the language of your Windows language settings β the display language and the preferred-languages list are read from the registry β and falls back to English. -Lang ru / -Lang en overrides the detection. The switch in the window header changes every label immediately.
Troubleshooting
"No gateway domain configured" β this copy was not published with a domain. Pass one: use Custom setup in the wizard, or
.\install.ps1 -Gateway <shared-domain>, or setMESH_PUBLIC_URL.Registration fails β the installer stops before writing anything when the gateway cannot be reached. Check that the domain resolves, then retry with an explicit
-Gateway.pythonopens the Microsoft Store β that is the App Execution Alias, not an interpreter. Both installers resolve a realpython.exeby full path and ignore the alias; the wizard never pins an alias into the autostart entry.The node does not come back after a reboot β run
.\ops\doctor.ps1: it prints the autostart interpreter, the heartbeat age, the lastagent.loglines (including a torn final line) and the gateway's own view of this node..\ops\windows\agent-watchdog.ps1performs that check once and restarts the agent; the scheduled taskRemoteHarnessMCPWatchdogruns it every five minutes, so a dead agent comes back on its own.
Standalone on Windows
The wizard does not offer it; use the console installer:
.\install.ps1 -Mode standalone -Port 8096That starts a local-only FastMCP server at http://localhost:8096/sse and puts that URL on the clipboard.
π± Android installation (Termux)
The same install.sh installs a node on an unrooted phone or tablet, and the phone
then shows up in Gemini Spark like any other machine. Full guide:
docs/TERMUX.md.
# 1. Termux from F-Droid (not Google Play); optionally Termux:Boot and Termux:API
pkg update -y
# 2. the usual one-liner
curl -fsSL https://racknerd-5a24bf9.merino-carob.ts.net/install.sh | bashWhat a phone changes, and how the installer handles it:
Packages |
|
Node name | The device model ( |
Domain file |
|
Autostart | A runit service ( |
Clipboard |
|
Two Android-side switches are yours to flip: installTermux:Boot and open it once, and set battery optimisation to Unrestricted for Termux. Without them Android unloads the node with the screen off.
The phone must also be able to resolve the gateway name. A gateway that lives on a private network (Tailscale, a VPN, a DNS override on your laptop) resolves there and nowhere else β mobile data resolves nothing private. Put the phone on that network, or give the gateway a publicly resolvable domain; the installer detects a name it cannot resolve and says so before writing anything: see docs/TERMUX.md.
sv status rhm-agent # is it up?
sv restart rhm-agent # restart now
tail -f $PREFIX/var/log/sv/rhm-agent/current--mode=standalone also works, but it serves 127.0.0.1 only β reachable from
inside that phone alone. Use the default tunnel mode to drive the phone from Gemini.
π Automatic updates
A node installs itself from a GitHub release and then keeps itself current the same way. You do not re-install anything by hand, and nothing unverified is installed.
It checks by itself. A running agent asks the GitHub Releases API every 6 hours (
MESH_UPDATE_CHECK_INTERVAL), compares the tag with the version incore/version.pyand installs what it finds. On Windows the dailyRemoteHarnessMCPUpdatertask does the same for a machine whose agent is not running.MESH_UPDATE_AUTO=0turns installing off and keeps reporting.It verifies before it installs. The payload is downloaded, its SHA-256 is compared with the published checksum, and only then are
core/,skills/andops/replaced. No checksum, no update.It can always go back. The replaced directories are saved under
%USERPROFILE%\.config\remote-harness-mcp\backups\<version>-<timestamp>\, and an update interrupted by a kill or a power cut is rolled back automatically on the next start.The node comes back new. The agent is restarted through whatever supervises it (the watchdog task on Windows,
systemctl --user restart rhm-agent.service,launchctl kickstart -k) β or started directly when nothing supervises it.
By hand, any time:
remote-harness-mcp update -Check # is a newer release published?
remote-harness-mcp update # install it now and restart
remote-harness-mcp update -Check -JsonFrom Gemini: ask the node to run mesh_update (status, then check, then
apply).
The state of the last check is in the heartbeat, so ops/doctor.ps1 shows it, and
the full history is in %USERPROFILE%\.config\remote-harness-mcp\update.log.
Configuration, exit codes, rollback and troubleshooting: docs/UPDATES.md.
π One shared domain (canonical URL contract)
A node is never addressed by its own hostname. The gateway publishes a single shared domain, and the node name travels as a query parameter:
Purpose | Canonical URL |
MCP over SSE |
|
MCP Streamable HTTP |
|
Reverse tunnel (agent) |
|
Why: every extra hostname would need its own DNS record and its own SAN in the TLS certificate. A node whose name is missing from the certificate fails the TLS handshake, and the Gemini client then reports an opaque "cannot connect to host". With one shared domain the certificate covers every node forever, and adding a node is only a registration call.
The shared domain defaults to
smart-server.online; override it with./install.sh --domain=<shared-domain>(node side) orMESH_PUBLIC_URL(gateway side).Legacy per-device subdomain URLs still resolve for backwards compatibility and log a deprecation warning; set
MESH_LEGACY_SUBDOMAIN=0on the gateway to reject them outright.The installer now records the domain it resolved in the domain file, so the node's share links, its tunnel and the gateway all name the same host:
MESH_PUBLIC_URL=https://<shared-domain>indomain.env, written in both install branches (standalone, and cloud-gateway/tunnel right afteragent.env). The value is normalised first, the__MESH_DOMAIN__placeholder and an empty value are never written, the write is skipped when the file already names that host, and a file that cannot be written is not fatal β the installer prints the exact command to run by hand. The built-in default is never recorded at all: it is a fallback, not a configuration, and pinning it would stopcore/domain.pyfrom consulting the legacyMESH_GATEWAYat all, so a later change inagent.envwould be silently ignored β nothing is lost, because with no file that same value is already the resolver's last fallback.--dry-runreports the file and the value it would write β includingnot written (the built-in default is a fallback, not a configuration)β and still changes nothing on disk.Why this matters: share links are built by
core/domain.py, whose chain isMESH_PUBLIC_URLβAGY_PUBLIC_BASE_URLβ the domain file β the built-in default, and which deliberately never reads the legacyMESH_GATEWAYthatagent.envcarries (onlygateway_host(), the tunnel host, does). A node installed the normal way βagent.envwithMESH_GATEWAY/MESH_USER/MESH_TOKENand no domain file β therefore dialled the right gateway while minting links on the built-in default. On an already-installed node, re-run the installer or write the one line by hand:mkdir -p ~/.config/remote-harness-mcp && echo 'MESH_PUBLIC_URL=https://<domain>' > ~/.config/remote-harness-mcp/domain.env, or the same withsudo tee /etc/remote-harness-mcp/domain.envon Linux. Existing share links keep working; new ones use the configured domain.MESH_DOMAIN_FILEonly overrides the path, and it belongs to the node's own configuration rather than to the installer invocation: keep it inagent.env(everyMESH_*key there is exported to the node), or the domain is written into a file the node never reads. The installer creates the parent directory of whichever file is in effect. Over--ssh=<host>the domain resolved here is handed to the remoteinstall.shas--domain=, so the target records that host instead of re-resolving it (a repository copy still carrying the__MESH_DOMAIN__placeholder would otherwise fall back to the built-in default).
π Security
Two credentials per node β the device token opens the tunnel and is accepted only in an
Authorization: Bearerheader; the link key (lk_β¦) is the only credential allowed in a URL. The device token in a query string is rejected with HTTP 401.Link keys rotate β
POST /v1/devices/<node>/rotateissues a new link key and kills the old URL;POST /v1/devices/<node>/revokeremoves the node. Neither needs a reinstall.The device token rotates too β no reinstall. From the node's payload directory run
python3 -m core.rekey(Windows:cd $env:USERPROFILE\.remote-harness-mcp; python -m core.rekey). It asks the gateway for a new token, writes it intoagent.env, the launchd plist on macOS and the Startup launcher on Windows, restarts the supervisor (systemd / launchd / Scheduled Task / runit) and waits for the node to report online. The previous token keeps working until the node connects with the new one, so a failed restart restores the old token instead of locking the machine out.POST /v1/devices/<node>/rotate-token/finishretires it immediately when the old token is known to be in someone else's hands. Theremote-harness-mcp rotateshorthand is the same command through the npm CLI.Enrolment β
/api/registerrefuses to hand a token to a request that only knows a name or a MAC address; possession of the device token is required to reuse a registration. SetMESH_REGISTER_MODE=pair(withMESH_ADMIN_TOKEN) to replace self-registration entirely with the pairing flow:POST /v1/devices/pairβ approve the code β collect the credentials.Outbound-only tunnel β nodes never listen on public ports; the agent dials out to the gateway.
Gateway is a relay β it terminates TLS and forwards calls; commands run only on your node.
Unprivileged by default β the agent runs in user space, without root.
Whoever has your MCP link can run commands on that machine. Treat it like a password: don't publish it, and rotate the link key (POST /v1/devices/<node>/rotate) if it leaks.
π’ Self-hosting the gateway
The gateway is a single host serving every node. Publish it only through the deploy script, so the running gateway, the served installers and the node bootstrap payload cannot drift apart:
MESH_GATEWAY_SSH=root@<gateway-ip> ./deploy_gateway.sh # deploy
MESH_GATEWAY_SSH=root@<gateway-ip> ./deploy_gateway.sh --dry-run # preview
MESH_GATEWAY_SSH=root@<gateway-ip> MESH_GATEWAY_SSH_PASS_FILE=~/.ssh/gw-pass \
./deploy_gateway.sh # password authIt verifies every upload with an sha256 manifest on the host, takes a timestamped backup, installs with the right owner, restarts the service and finishes with a public health check. Nothing is installed if verification fails.
One place for the domain. MESH_PUBLIC_URL in domain.env
(/etc/remote-harness-mcp/domain.env on Linux, %USERPROFILE%\.config\remote-harness-mcp\domain.env
on Windows) is the only setting that names it: the node, the gateway, the installers, the public
file links and the nginx vhosts all resolve it through core/domain.py. Switching domains is one
value plus ops/nginx/render-domain.sh --apply β see docs/DOMAIN.md.
install.ps1 encoding β one file, two representations. The repository copy is UTF-8 with a BOM: Windows PowerShell 5.1 decodes a BOM-less script with the ANSI code page, the Russian strings then turn into smart quotes and the parser rejects the whole file, so .\install.ps1 from a clone would not run. The copy the gateway serves is written without the BOM by deploy_gateway.sh, because irm β¦ | iex receives the BOM as part of the first token and param(...) then stops being the first statement. nginx declares charset utf-8 for that location, so the BOM-less copy still decodes correctly. Please keep both properties when editing the file.
TLS certificate policy β one name, no per-device SANs. The certificate must cover the shared domain only; a node is selected by ?user=, never by a hostname:
# what the certificate currently covers
sudo certbot certificates | grep -A1 'Certificate Name: smart-server.online'
# canonical renewal check (staging, does not touch the live certificate)
sudo certbot renew --dry-run --cert-name smart-server.onlineLegacy per-device SANs may still be present from before this contract. They are harmless and keep old bookmarked URLs working; drop them at the next renewal once no client uses subdomain URLs any more (see the domains = line in /etc/letsencrypt/renewal/<name>.conf, or re-issue with a single -d).
π License
MIT β see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Give your AI hands on every computer you own. Remote control of your Windows, Mac and Linux machines from one ChatGPT, Claude or Cursor chat β even from your phone. Ask it to find out why the office server is crawling, update every PC in the house, sort out your mom's laptop from another city, or ship the site from your work machine while you're on a train. It runs commands, reads files and takes a screenshot when it needs to see the screen. You decide what each computer allows, and you can cut any of them off in a second.
- DNS-DATA RemoteDeprecatedcom.dns-data
Connect AI assistants to authorized Windows and macOS devices for files, commands, and management.
Connect AI assistants to explicitly paired computers, servers, and VMs for permitted remote actions.
Reach your own computers from any AI app: files, search, commands, approvals and outage alerts.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI services like Claude and Cursor to remotely control a Mac by executing shell commands, managing files, and running AppleScript for UI automation. Access is secured through OAuth 2.0 authentication and encrypted tunnels to protect remote interactions.2MIT
- AlicenseAqualityBmaintenanceEnables an AI agent to take full control of a machine β running shell commands, editing files, managing git, packages and processes, and reaching the network β with batched execution, server-side variables and selectable tool profiles that keep token usage low. It also speaks HTTP so the same agent can operate a remote build box, NAS or VPS.262MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI clients to securely reach and control tools running on your own computers through a relay and outbound device agent.MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants like Claude, Codex and ChatGPT to actually operate a computer β finding files, running commands, taking screenshots and driving a browser β and, after a one-time pairing, to help on family members' computers anywhere. Connections between machines are end-to-end encrypted through a relay, requiring no VPN, port forwarding or router setup.1MIT