remote-dev
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@remote-devrun make build in /vllm-workspace on the remote server"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
remote-dev
A remote development substrate for coding agents. It makes a remote Linux host reachable over SSH feel like the local working tree: every native editor tool has a remote twin with the same semantics plus endpoint fields.
Install the vaws-remote-dev package. The import package is remote_dev;
the console entry is remote-dev.
Local tool | Remote tool | CLI |
Read |
|
|
Write |
|
|
Edit |
|
|
MultiEdit |
|
|
Bash |
|
|
Glob |
|
|
Grep |
|
|
LS |
|
|
apply_patch |
|
|
write_stdin |
|
|
Plus background jobs (remote.job_status / job_tail / job_stop /
job_stdin), artifacts (remote.artifact_manifest / artifact_pull /
artifact_push), and endpoint facts (remote.probe,
remote.context_snapshot). Nineteen tools in total, served by one stdio MCP
server (remote-dev server) and mirrored one-to-one by CLI subcommands.
python -m remote_dev is equivalent to remote-dev.
Help and parser errors do not import execution backends or contact endpoints. The selected operation loads its implementation after argument parsing.
remote-dev probe --host <host> --diagnose-connection sends one fixed read-only
probe for OS, working directory and Python version. It reports local preparation,
SSH process duration, received TCP/authentication milestones, remote probe
execution and final stream drain/exit timing. Connection time includes client
startup and authentication; TCP time is a subset, not an additional phase.
Absent milestones and pure transfer time remain unknown. The unattributed
remainder includes channel/Python startup and transport overhead. Verbose SSH
lines are interpreted locally and excluded from the returned diagnostic stderr.
On a failed shared connection, the fixed probe may compare an independent
connection. It never retries an arbitrary business command. Ordinary bash
results also include transport timings without enabling verbose SSH tracing.
Runtime requirements: Python 3.9+ and an ssh client. No third-party
packages. Nothing here needs GPU/NPU hardware; the remote host only needs
bash, python3, and (for remote.apply_patch unified diffs) git.
The same public API runs from Windows, macOS and Linux clients. Attached SSH streams and local forwards launch literal argv under an owned local process group: a Windows Job Object is assigned before the child starts; POSIX uses an independent session. Timeout and close include inherited children even after SSH itself exits, and preserve available UTF-8 output. The internal launcher accepts native cwd and an environment overlay; it does not interpret shell syntax or translate Windows/WSL paths. The remote command still runs in Bash on its Linux endpoint. Local SSH cleanup does not prove remote process quiet; managed remote jobs use their supervisor's stop and quiet receipt. A POSIX child that deliberately creates a separate session is outside the local group.
Install and start the MCP server
From a git ref (no local checkout required):
uvx --from git+https://github.com/vllm-ascend-workspace/remote-dev@main remote-dev serverFrom a clone:
uv pip install -e .
remote-dev server.mcp.json (Claude Code / Cursor; Kimi Code uses .kimi-code/mcp.json —
see examples/kimi-mcp.example.json):
{
"mcpServers": {
"remote-dev": {
"type": "stdio",
"command": "uvx",
"args": [
"--from",
"git+https://github.com/vllm-ascend-workspace/remote-dev@main",
"remote-dev",
"server"
],
"env": {
"REMOTE_DEV_DEFAULT_USER": "root",
"REMOTE_DEV_DEFAULT_ROOT": "/",
"REMOTE_DEV_DEFAULT_CWD": "/",
"REMOTE_DEV_RESOLVERS": "/absolute/path/to/consumer/remote_dev_plugin.py:setup"
}
}
}
}If the package is already installed in the client environment, command can
be remote-dev with args: ["server"]. More client examples live in
examples/. See CLIENT_COMPATIBILITY.md for
per-client notes.
Related MCP server: ssh-mcp
Native-habit compatibility layer
One execution kernel serves every client; the differences live in a thin,
shared parameter layer (remote_dev.mcp.schemas.normalize_arguments, applied
by both the MCP dispatcher and the CLI --input-json path):
Aliases:
pathforfile_path(read/write/edit/multi_edit),line_offset/n_linesforoffset/limit(read),cmd/workdirforcommand/cwd(bash),session_idforjob_id,-i/-A/-B/-C/-n/head_limit(grep). A canonical key always wins; unknown keys pass through. Fields with aliases are enforced by the server, not the wire schema'srequiredlist, so alias-only calls pass provider-side validation.remote.readaccepts a negativeoffsetto read from the end of the file (offset=-100reads the last 100 lines). Binary files (NUL byte in the first 8192 bytes) fail with an actionablebinary_filestatus — there is no remote image/media preview; useremote.bashorremote.artifact_pull.remote.writegainsappend(Kimimode=append): extends the file atomically, creates it when missing, and is mutually exclusive withoverwrite.remote.grepgainscase_insensitive,context_lines/before_context/after_context, aline_numberstoggle,offsetpagination, andinclude_ignored(rg--no-ignore --hidden; the grep fallback only lifts its hidden/.git excludes and says so).output_modedistinguishescount(matching lines per file,rg -c) fromcount_matches(total matches per file,rg --count-matches; the grep fallback counts-omatches per file). They differ whenever one line holds several matches.remote.bash command=...starts with writable stdin and waits up toyield_time_ms(default 10000) for output or completion. A live process or unread output returnssession_id; completion reportsexit_codeandquiet. The wait excludes SSH connection and process preparation. Omittedtimeout_ms(or zero) means no command deadline; an explicit value limits remote execution.run_in_background,interactive, and the separate monitor tool have been removed.remote.job_stdin session_id=... chars=...writes input; emptycharspolls any session, including completed sessions with unread output. Per-stream byte cursors advance under a cross-process lock, preserving Unicode and preventing concurrent polls from replaying bytes. A partial input write reportswritten_chars; resend that exact remainder. EOF is deferred until all submitted characters are accepted.tty=trueallocates a real 24x80 remote PTY with merged stdout/stderr. Ctrl-C signals the terminal foreground group.eof=truesends terminal Ctrl-D (canonical terminal semantics); pipe EOF closes stdin. In pipe mode, control bytes remain data.remote.job_stopstops the owned process family.max_output_tokensbudgets approximately four UTF-8 bytes per token across both text and structured previews, shared by stdout/stderr. Each preview has a four-byte minimum so one Unicode character can progress. Status/refs metadata is separate. Unreturned bytes remain at the cursor; full decoded logs accumulate in local refs as pages are consumed.Developer calls reuse one binary SSH stdio connection on Windows and POSIX, independently of OpenSSH ControlMaster. Concurrent first calls to the same endpoint share startup; different endpoints do not hold a global startup lock. The 32-connection pool evicts idle LRU entries automatically and expires idle connections after five minutes (reaped within another minute). Busy connections are never evicted; capacity waits respect cancellation and the request deadline. Both MCP and remote RPC reserve two workers/eight slots for status, stop, tail and short stdin exchanges, alongside eight ordinary workers/32 slots. Long waits cannot consume that control capacity. A lost reply remains an unknown outcome and is never automatically replayed.
Explicit
runtime_env_fileruns once per command in that command's Bash process, preserving functions, non-exported variables, PATH order and shell options. Missing scripts or failed initialization prevent the user command. Normal Bash startup (including BASH_ENV and SSH .bashrc behavior) is retained; arbitrary dynamic initialization is never cached.Managed callers using
processes.control(..., "prepare", spec=...)can setprepared_timeout_seconds(default 120, between 1 and 86400 seconds) for their bounded queue/activation wait. Expiration cancels the unopened gate without running user code. Commandtimeout_secondsstarts after activation; a lease heartbeat does not implicitly extend the remote prepared deadline.Tool arguments outside the published schema, native aliases and registered endpoint selectors are rejected before execution. MCP
remote.bashusesyield_time_msand continuation throughsession_id;wait=Trueis an SDK option and is rejected on the MCP surface.Developer MCP and CLI tools choose pooled connections and keepalives internally. Their endpoint arguments no longer include
ssh_mux,keepaliveor--long-stream; low-level Python transport callers retainEndpointpolicy andEndpoint.for_long_stream. MCP failure text includes the status and recovery detail for clients that do not read structured results.Reads scan in bounded memory.
verify_content=false(CLI--no-verify-content) stops after a positive-offset log window and omits the hash/read ledger; the default retains a full hash and exact line count for guarded editing. Search results stream to a bounded page. Artifact batches use one SSH stream with 1 MiB chunks, checksum verification and atomic file replacement. Default probes avoid importing application modules; request explicitmodules(CLI--module) when needed.
The endpoint-explicit contract
remote-dev resolves endpoints from explicit fields and nothing else:
Field | Default | Meaning |
| required | SSH host |
| required | SSH port |
|
| SSH user ( |
|
| Path-policy root ( |
| same as | Default working dir ( |
|
| Source |
| unset | Remote profile script ( |
| unset | SSH private key |
|
| SSH connect timeout |
| unset | Name from the endpoint alias files |
The Python Endpoint API additionally accepts ssh_mux and keepalive
for low-level transport callers; these are not developer-tool arguments.
Resolution order in remote_dev.core.endpoint.resolve_endpoint:
host+portgiven: use them directly.aliasgiven: look it up in the alias files (REMOTE_DEV_ENDPOINTS_FILE, thenendpoints.jsonandendpoints.local.jsonin the process cwd; both are git-ignored) and let explicit caller fields override the alias entry.Otherwise ask each registered resolver plugin, in registration order.
Nothing claimed the payload: fail with
EndpointErrorlisting the known selector fields and registered resolvers. remote-dev never guesses a target from the working directory or from files it does not own.
remote.job_status, remote.job_tail and remote.job_stop can rebuild
their endpoint from the local job record and therefore resolve only when the
caller supplies a selector.
Permission model
Direct endpoints default to full remote-path permission (root=/) with
the same path as the default cwd. Path containment, symlink checks and
cwd validation are still enforced, but against / unless a narrower root
is set. Pass --root /srv/app --cwd /srv/app when a task requires path
isolation. A consumer that wants a project default cwd sets
REMOTE_DEV_DEFAULT_CWD; remote-dev does not assume a workspace tree.
Hook guards (remote_dev.hooks) default to allow and only observe; they
are the place to add policy if you need it.
Read ledgers are optional optimistic-concurrency checks scoped by
client_context_id, then CLAUDE_SESSION_ID, CODEX_SESSION_ID,
CODEX_RUN_ID, REMOTE_DEV_SESSION_ID. The MCP server sets a process-local
REMOTE_DEV_SESSION_ID at startup. Each nonempty effective context id is
stored under one filesystem-safe directory id-<sha256 of the raw id>. The
no-context fallback remains default and does not collide with a caller who
explicitly supplies that word. A ledger is never required for an edit or
write of a file this context has not previously read. After a successful
read, later writes in that context are checked against the recorded SHA.
Ledgers written by earlier encodings are kept in place. A write or edit that
finds only those older files — including a v1 record occupying the current
id-<sha256> or default path — returns structured read_required until
the same context reads the file again. Path or ledger_scope matching the
current computed string is not enough: only a record written under the
current encoding (schema remote-dev.read_ledger.v2) authorizes a later
write. A legacy shared SHA is not authorization for a distinct context.
Resolver plugin interface
Anything that is not host/port/alias - session registries, machine
inventories, worktree bindings, coordinator leases - is consumer knowledge.
The consumer registers a resolver with remote-dev; remote-dev never imports
the consumer.
# consumer/remote_dev_plugin.py
from remote_dev.core.endpoint import EndpointError, register_resolver, resolver_setup
def by_lab(payload):
lab = payload.get("lab")
if not lab:
return None # not ours: next resolver, please
record = my_inventory.load(lab) # consumer-owned lookup
if record is None:
raise EndpointError(f"unknown lab {lab!r}")
return {
"host": record.host, "port": record.ssh_port,
"cwd": record.root,
"runtime_env_file": "/etc/profile.d/toolchain.sh",
"kind": "lab-endpoint",
"source": {"lab": lab},
}
@resolver_setup
def setup():
register_resolver(by_lab, name="labs", fields=("lab",))Contract:
resolve(payload) -> dict | Endpoint | None. ReturnNoneto decline. Adictneedshostandport; remote-dev builds theEndpoint, lets explicit caller fields (user,root,cwd,runtime_env,runtime_env_file,identity_file,connect_timeout_ms,ssh_mux,keepalive) override the resolver's values, setskindtoresolver:<name>unless provided, and recordssource.resolver.fieldsdeclares the payload keys the resolver claims. They are added toselector_fields()sohas_selector()and the job tools treat them as "an endpoint was requested". Tool schemas keepadditionalProperties: true, so consumer keys pass through MCP clients untouched.Resolvers are consulted even for an empty payload. A consumer that wants "zero-argument inside my worktree" behaviour implements it in its resolver (return
Nonewhen there is no binding).Raise
EndpointErrorwhen you claim a payload but cannot resolve it. Other exceptions are wrapped intoEndpointErrorwith the resolver name.
Loading into a process you do not control (the MCP server is spawned by the client, CLI wrappers by a shell):
REMOTE_DEV_RESOLVERS="/abs/path/consumer/remote_dev_plugin.py:setup"
REMOTE_DEV_RESOLVERS="consumer.remote_dev_plugin:setup,other.pkg:setup"Entries are module:callable or /path/file.py:callable. A callable marked
with @resolver_setup is invoked once and registers what it likes; any other
callable is registered directly under its spec string. A broken entry fails
every resolution with the same message instead of degrading to "no
resolvers". examples/resolver_plugin.py is a complete runnable example.
Programmatic embedding works too: from remote_dev.core.endpoint import register_resolver before invoking remote_dev.mcp.tools.call_tool.
The public result envelope is remote_dev.result (schema_version:
remote-dev.result.v1). Schema JSON ships as package data.
Selector keys on the CLI travel through --selector KEY=VALUE:
remote-dev bash --selector lab=gpu-1 --command 'nproc'What this repository does not hold
No consumer state.
state/(job records, read ledgers, logs, artifact manifests) is git-ignored and relocatable withREMOTE_DEV_STATE_DIR. The default is<cwd>/state.No endpoint data.
endpoints.json/endpoints.local.jsonare git-ignored; ship aliases from your own tree viaREMOTE_DEV_ENDPOINTS_FILE.examples/endpoints.example.jsonshows the shape.No knowledge of any particular consumer: no session registries, machine inventories, coordinators, task facades, or skill catalogues. Those live in the consumer and reach remote-dev only through resolvers and environment variables listed below.
Configuration reference
Variable | Purpose |
| Default |
| Default |
| Default |
| Default |
| Comma-separated resolver plugin specs |
| Alias file(s), |
| Local state directory (default |
| OpenSSH ControlMaster dir (default |
| Process-wide SSH multiplexing default on POSIX: unset or |
| Read-ledger scope when no client id is given |
For low-level Python SSH calls, REMOTE_DEV_SSH_MUX is the process-wide
default and is read without changing
global SSH configuration or the shared ControlMaster socket. Leave it unset or
set it to 1 to keep today's shared-mux path, including the per-identity
ControlPath suffix. Set it to exact 0 in a Python process that must not join
the shared master (ControlMaster=no, ControlPath=none, ControlPersist=no
on every SSH invocation from that process that does not set ssh_mux).
Accepted values are unset, 1, and 0; any other value is a configuration
error. Developer MCP/CLI operations use their own pooled independent
connections with keepalives, regardless of this low-level default.
Native Windows CLI and MCP pipes use UTF-8. Shell script uploads preserve LF
bytes, and nested artifact paths use POSIX separators on the Linux peer.
On native Windows the transport chooses independent connections by itself
because Win32-OpenSSH does not implement Client ControlMaster; do not set
ssh_mux=true or REMOTE_DEV_SSH_MUX=1 there.
A single Python process may do both at once. Set ssh_mux=false
on the endpoints that must stay off the shared master, and leave the rest on
the default. This is not optional for long-lived connections such as
ssh -N -L tunnels: ControlMaster delegates -N forwards to the mux master
and the client exits rc=0 immediately, tearing the tunnel down. OpenSSH
first-option-wins semantics make a later ControlMaster=no override
ineffective, so the independent triple has to be chosen before the command is
built. ControlMaster=no alone is not enough — a client can still attach to
an existing ControlPath.
Low-level keepalive=true adds ServerAliveInterval=30 and
ServerAliveCountMax=10. That is a mechanism flag, orthogonal to mux, and
conditional rather than always-on: a slow multi-hour stream otherwise dies
to an idle timeout somewhere in the path, but attaching ServerAlive to short
multiplexed commands would set TCP keepalive policy on the shared
ControlMaster (the master owns the TCP connection; first-option-wins).
Hour-scale streams and ssh -N -L tunnels use one named entry point:
Endpoint.for_long_stream(host, port, ...). It always sets ssh_mux=False
and keepalive=True and cannot be half-configured (ssh_mux=True is
refused). run_stream /
stream_ssh_command refuse any endpoint that would still attach to a
ControlMaster — the silent failure this project recorded is rc=0 with the
tunnel gone, so a docstring is not a control.
Live streaming is the library function remote_dev.core.ssh_transport.run_stream.
It stays attached, forwards output as it arrives, and enforces a timeout on
both sides (remote timeout --preserve-status plus a local deadline-bounded
reader: select on POSIX, reader threads on native Windows). It returns
RemoteCompleted (returncode, not exit_code) and does not emit
remote-dev.result.v1. It is not remote.job_*.
Scripts travel through binary stdin instead of command-line arguments, so
large generated scripts work on native Windows. Upload and output draining
run concurrently under the same local timeout.
Detached background work uses one process implementation:
remote_dev.processes.control(endpoint, job_id, action, **parameters).
Actions are prepare, go, status, tail, stop, stdin, launch, and
exchange. The Linux worker
is a child-subreaper with identity/marker checks, a start gate, descendant
drain, and timeout/stop. Ordinary remote.bash and
remote.job_* call this same boundary. Coordinator may call it directly
with an explicit host+port mapping; remote-dev does not load coordinator
state. The worker is Linux-only; the local transport client supports
macOS, Linux, and native Windows.
Python consumers needing a completed result call remote_bash(..., wait=True).
This waits and drains full log refs through the same supervisor, with stdin
closed unless a PTY is requested. Coordinator retains its separate
prepare/authorized go gate for managed execution.
Two more transport primitives close the remaining SSH-option gaps. They are
library APIs, not MCP tools, and they do not accept extra -o strings.
open_local_forward(endpoint, remote_port)opensssh -N -Lon thefor_long_streamshape (ControlMaster=no,ControlPath=none,ControlPersist=no, keepalives,ExitOnForwardFailure=yes). It refuses a multiplexed endpoint the same wayrun_streamdoes. The handle exposeslocal_port,wait_ready(timeout_s), andclose()(process-group kill; a forward that dies is never reported as rc=0 — that silent success is the recorded mux-absorbed-Nfailure).run_interactive(endpoint, remote_command)is a one-off TTY-inherited bootstrap (BatchMode=no, password/keyboard-interactive only,PubkeyAuthentication=no). Combining it with multiplexing is impossible: a password prompt through a ControlMaster is meaningless and hangs.interactive_ssh_commandreturns the argv for wrappers such asSSH_ASKPASS. This is first-contact bootstrap, not a general PTY facility.
Stdin bytes into a remote command are already run_bytes. Detached
background work is remote.bash --run-in-background / remote.job_*.
Directory trees move with remote.artifact_push / artifact_pull.
MCP server and clients
remote-dev server speaks JSON-RPC over stdio with Content-Length framing
(newline-delimited JSON is accepted as a test fallback). Discovery advertises
portable underscore names (remote_read, ...); dotted canonical names remain
accepted on tools/call. Resources:
remote://endpointsremote://endpoint/<endpoint-id>/context/latestremote://endpoint/<endpoint-id>/jobsremote://endpoint/<endpoint-id>/job/<job-id>/{status,stdout,stderr}remote://endpoint/<endpoint-id>/artifactsremote://endpoint/<endpoint-id>/artifacts/<artifact-id>/manifest
Every tool returns {"text": ..., "result": ...} where result follows
remote_dev.result / the packaged result.schema.json (remote-dev.result.v1):
outcome in success | needs_input | blocked | failed | timeout | cancelled,
a compact preview, and refs to full logs on disk. Model-visible text is
capped; full output is reachable through refs and MCP resources.
Validation
uv pip install -e ".[test]"
python -m pytest
remote-dev validate --local-onlyLive checks need a reachable SSH host:
remote-dev validate --host <host> --port <port> --root /srv/app --cwd /srv/app
remote-dev validate --alias lab --skip-localThe validator compile-checks the installed package, reports MCP/CLI burden metrics, and (with an endpoint) exercises read/edit/write/bash/search, patches, artifacts, background jobs, MCP resources and parallel scratch workers, then cleans up after itself.
Layout
remote_dev/ installable package (core, processes, mcp, hooks, tools, schemas)
tests/ unittest suite collected by pytest (mocked transports, no SSH)
examples/ client configs, alias file shape, resolver pluginSee DESIGN.md for the architecture and VALIDATION.md for the evidence record.
License
MIT. See LICENSE.
Runtime feedback and connection checks
MCP responses report the process-start package identity separately from the
currently installed distribution. A restart_required status means the native
client must restart that MCP server; changing installed files does not reload it.
The source commit is unknown when distribution metadata does not provide it.
remote-dev probe --host HOST --port PORT --diagnose-connection runs a fixed,
read-only SSH probe. After a failed multiplexed probe it compares an independent
connection, and may suggest ssh_mux=false. It never replays the business command
or changes global SSH configuration. Foreground bash results expose the actual
connection mode and timeout. A timeout or exit 255 leaves remote outcome unknown.
remote_dev.diagnostics.open_http selects direct or environment proxy use
per call. Its companion diagnostics strip URL credentials and query parameters,
and distinguish HTTP status from DNS, timeout and connection errors.
This server cannot be deployed
Maintenance
Related MCP Connectors
Remote MCP learning coach for coding agents.
Roadmap, tasks, releases and user feedback your coding agent reads and writes over MCP.
Run commands and read/write files on your servers over Termalin's keyless tunnels (hosted MCP).
Agent-first web hosting: deploy sites, apps, databases and domains over MCP.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables agent-style remote development over SSH with file operations and command execution, exposing native tools for searching, editing, and transferring files on remote machines.1-
- AlicenseAqualityCmaintenanceEnables reading, writing, editing, searching, running commands, transferring files, and using git on a remote Linux server over SSH via MCP tools.223AGPL 3.0
- FlicenseNot gradedqualityCmaintenanceEnables secure remote workspace management over SSH, supporting file operations, shell commands, and profile configuration for agent-safe remote code environments.-
- AlicenseNot gradedqualityAmaintenanceGives MCP-compatible AI clients safe, hands-on access to local codebases: file reading/search, multi-file patches, command execution, interactive sessions, Git inspection, and coordination of local agent providers such as Antigravity and OpenCode.18Apache 2.0