Skip to main content
Glama

HTTP command relay

Ephemeral HTTP command relay. Push a shell command from any HTTP client; have it run on a remote machine that has nothing installed but curl and bash.

Useful whenever you need to bounce a command to a shell without opening an SSH port, installing an agent, or deploying anything:

  • LLM coding sessions driving an arbitrary remote host - any tool, self-hosted or cloud (MCP hosts, curl-literate chat models, local runners, etc.)

  • CI runners poking at post-deploy state

  • Admin shells behind NAT / firewalls

  • Headless devices reachable only outbound

Data on the relay is minimised, not absent: each session gets a small directory holding the pending command/result and a timestamped push history, purged as soon as the idle TTL lapses (default 3h) or on explicit delete. Session keys are 128-bit random.

Architecture

  CLIENT                            RELAY                        REMOTE SHELL
  (LLM / CI / terminal)         (single vhost)                   (curl + bash)

   |                              |                              |
   |-- POST /api/session -------->|                              |
   |<-- key + URLs ---------------|                              |
   |                              |                              |
   |        (user pastes the remote-quickstart on the remote)    |
   |                              |                              |
   |                              |<-- curl /r/KEY?mode=... -----|
   |                              |   runner script starts       |
   |                              |                              |
   |-- POST /cmd-KEY ------------>|      queued on disk          |
   |<-- 201 ----------------------|                              |
   |   (client may disconnect)    |                              |
   |                              |<-- GET /cmd-KEY (poll) ------|
   |                              |-- 200 + command body ------->|
   |                              |                >>> prints cmd|
   |                              |                (y/N if supervised)
   |                              |                bash runs     |
   |                              |                              |
   |                              |<-- POST /result-KEY ---------|
   |                              |      queued on disk          |
   |                              |--- 201 --------------------->|
   |                              |                <<< done      |
   |-- GET /result-KEY (poll) --->|                              |
   |<-- 200 + output -------------|                              |

Related MCP server: ssh-mcp

Stack

Component

Role

First-party code

nginx

Reverse proxy, TLS, rate limiting, static landing page

Config only

PHP-FPM

Session API, runner script generator, file-backed queue

Yes

certbot

Automatic TLS issuance + renewal (optional, --profile tls)

No

No database. No worker. The only persistent state is a flat-file queue under data/sessions/<KEY>/ (bind-mounted from the host into the container at /var/data/sessions/<KEY>/), data-minimised and purged on idle TTL or explicit delete.

Quick start

cp .env.example .env
docker compose up -d

The shipped .env.example defaults to a self-consistent local HTTP setup - no editing required for a first run. After the command above, the API and the one-liners it hands out are both reachable at http://localhost:49180.

For a public deployment, edit .env first: set DOMAIN to your real hostname, SCHEME=https, NGINX_MODE=tls, HTTP_PORT=80 (required so Let's Encrypt's HTTP-01 challenge is reachable) and HTTPS_PORT=443, then:

docker compose --profile tls up -d         # HTTP + HTTPS with automatic TLS

That's the whole install. Everything else runs in containers.

Environment

All knobs live in .env; nothing is hardcoded.

Var

Default

Meaning

DOMAIN

localhost

Public hostname for the API / landing page

SCHEME

http

http or https - baked into returned one-liners

PUBLIC_PORT

49180

Port baked into generated URLs. Leave empty for the standard port of SCHEME (80/443)

NGINX_MODE

http

http or tls. tls requires --profile tls

HTTP_PORT / HTTPS_PORT

49180 / 49443

Host ports published by nginx. Public HTTPS deploy: 80 / 443 (80 is required for the ACME HTTP-01 challenge)

RATE_LIMIT

10r/m

Per-IP rate on POST /api/session (nginx limit_req syntax)

RATE_LIMIT_BURST

5

Burst slots before 503

PROXY_TIMEOUT

3600

Max seconds either side of a pipe waits for the other end. Must comfortably exceed LONGPOLL_MS

MAX_BODY_SIZE

25m

Single source of truth for body-size limits: nginx client_max_body_size on the queue endpoints, and the API derives its raw-body cap (post_max_size), memory limit, its gzip zip-bomb decoded-size cap (3x this value), and the runner's output-truncation cap (80% of this value) from it, so none of them can drift apart. 0 = unlimited at the nginx layer (PHP still keeps a finite decoded cap for safety)

LONGPOLL_MS

15000

Long-poll window (ms) GET /cmd-{key} / GET /result-{key} hold an empty slot open before returning 204, so listeners pick up work sub-second instead of on the next poll tick

FPM_MAX_CHILDREN

64

PHP-FPM pool size. Each active session pins ~2 workers for up to LONGPOLL_MS at a time, so this bounds how many sessions can be live concurrently before other requests start queueing

FPM_MAX_REQUESTS

500

Requests a PHP-FPM worker serves before it is recycled, so no slow leak accumulates over a long uptime. 0 = never recycle

SESSION_TTL

10800

Idle TTL for a session (seconds). Every request on the key resets it; expired sessions are purged with their queue + history.

AUDIT_LOG

0

1 = log key generation to container stderr. Independently of this, nginx's access log masks the key segment of every request line as <key>

SOURCE_URL

(empty)

Target of the landing page's source link and of GET /source. Empty hides the link

CERTBOT_EMAIL

(required for tls profile)

Contact address used when requesting certs from the ACME CA

CERTBOT_STAGING

0

1 = use the ACME staging environment (test-only certs, avoids rate limits)

The defaults above are a ready-to-run local HTTP setup - cp .env.example .env && docker compose up -d gives a working roundtrip at http://localhost:49180 with no edits. A public HTTPS deploy needs DOMAIN set to your real hostname, SCHEME=https, NGINX_MODE=tls, HTTP_PORT=80 (for the ACME HTTP-01 challenge) and HTTPS_PORT=443.

Behind your own reverse proxy? Skip the tls profile. Point your proxy at HTTP_PORT on localhost and terminate TLS there. SCHEME and PUBLIC_PORT describe what clients see, so behind a proxy on the standard port they are https and 443 (or empty), not the internal HTTP_PORT.

The CI deploy renders .env with scripts/render-env.sh: .env.example defaults overridden by same-named CI variables. For a non-localhost DOMAIN it refuses SCHEME=http unless ALLOW_PLAIN_HTTP=1, and when SCHEME is given without PUBLIC_PORT it uses the standard port of SCHEME, so a public deploy cannot silently hand out http://DOMAIN:49180/ one-liners.

API

POST /api/session

Generate a new session. Returns:

  • key - 32-char hex, 128-bit entropy

  • ttl_seconds - idle TTL (default 3h sliding; any request on the key resets it)

  • urls.cmd / urls.result - queue push/pop endpoints for this session

  • urls.runner - base URL of the runner script; append ?mode=auto for unattended mode

  • urls.api - /api/session/<key> for re-fetching the payload

  • remote_quickstart - ready-to-paste one-liner, supervised mode (y/N per command)

  • remote_quickstart_auto - the same line with ?mode=auto, for unattended hosts; show operators both and let them pick

  • exec - one-liner template with a COMMAND placeholder for pushing from any HTTP client

  • mcp_messages - agent-facing message templates for the MCP client ({version, templates}, from php/app/mcp-messages.json). Serving the wording from the relay means prompt iterations deploy with the relay instead of requiring an npm re-publish of remotify-mcp; clients keep baked-in fallbacks, so the field is optional. Tool names/schemas/descriptions stay in the npm package deliberately (host approval surface). Omitted if the file is missing or invalid.

GET /api/session/{key}

Re-fetch the same payload for a known key. Touches the session (resets its idle timer) without consuming anything queued.

GET /api/session/{key}/status

Cheap probe: returns {cmd_queued, result_queued, cmd_in_flight, cmd_in_flight_seconds_ago, listener_seen_seconds_ago} without consuming anything:

  • cmd_queued / result_queued - whether a command/result is currently sitting in the hot slot.

  • cmd_in_flight - a command was picked up by the listener and no result has been pushed back yet.

  • cmd_in_flight_seconds_ago - seconds since that pickup (null when nothing is in flight).

  • listener_seen_seconds_ago - seconds since the listener last polled GET /cmd-{key} (null if it never has).

Used by the MCP server to detect whether the listener has already picked up the queued command.

POST /api/session/{key}/reset

Recovery endpoint: drops any queued command, any queued result, and clears the in-flight marker, while keeping the session (key, TTL, connected listener) alive. Returns what it actually cleared: {"reset": true, "cleared": {"cmd_queued": ..., "result_queued": ..., "cmd_was_in_flight": ...}}. Rarely needed by hand - the relay self-heals a wedged in-flight marker as soon as the listener reconnects, and the MCP server resets stale state on its own - but useful for scripts and as an operator escape hatch.

DELETE /api/session/{key}

Purges the session directory (queued cmd/result + any archived pushes) and returns 204.

POST /cmd-{key} / GET /cmd-{key}

POST pushes a command (plain bytes or Content-Encoding: gzip). GET consumes the hot command slot - used by the listener. Last-write-wins: pushing while a command is already queued replaces it; the old bytes survive as a timestamped archive.

GET long-polls: if the slot is empty the PHP worker holds the connection open (up to LONGPOLL_MS, default 15s) and returns the instant a command is pushed - sub-second pickup in practice - or 204 once the deadline passes. Pass ?nowait=1 to disable the hold and get an immediate 204/body, useful for synchronous probes.

A GET also self-heals a wedged session: polling for new work while a command is still marked in-flight proves that command's executor is gone (an executing listener never polls), so the relay clears the marker and queues a synthetic [remotify: ...] result for whoever is still waiting on it. A listener that died mid-command (kill -9, closed terminal, reboot) therefore stops blocking the session the moment it - or a replacement - reconnects.

DELETE /cmd-{key}

Drops the hot command slot without consuming (no archive, no listener side effects). Used by the MCP server to cancel its own queued command when it gives up waiting for a listener, so the command won't execute as an orphan when one eventually connects.

POST /result-{key} / GET /result-{key}

Symmetric endpoints for the listener to post results and the client to fetch them. GET /result-{key} long-polls exactly like GET /cmd-{key} (same LONGPOLL_MS window, same ?nowait=1 escape hatch).

GET /r/{key}?mode=supervised|auto

Returns a ready-to-bash runner script. The remote operator runs:

curl -fsSL 'https://relay.remotify.run/r/KEY' | bash              # supervised
curl -fsSL 'https://relay.remotify.run/r/KEY?mode=auto' | bash    # auto

supervised previews every incoming command and waits for y/N (accepts y, Y, yes, etc. - any response starting with y/Y). auto trusts every command (only on hosts where the session key is fully private).

Both modes print each incoming command as >>> <cmd> before running it and <<< done (N bytes pushed back) after the result is posted back, so the operator can see traffic even in auto mode.

The runner exports a non-interactive env block (DEBIAN_FRONTEND=noninteractive, CI=true, TERM=dumb, PAGER=cat, SYSTEMD_PAGER=cat, GIT_TERMINAL_PROMPT=0) before executing each command, so apt, pagers, and git don't block on TTY input. Sudo still prompts unless the caller uses sudo -n - the operator sees the preview line and can choose to authenticate.

supervised mode needs an attached terminal to ask y/N; if none is present (nohup, CI, piped in from something other than an interactive shell) the runner exits immediately with a message pointing at ?mode=auto instead of hanging. The output of any command that exits non-zero gets a trailing [remotify: exit status N] marker so the client can tell success from failure; very large output is truncated to a head plus a marker rather than dropped or rejected outright. Result pushes are gzip-compressed when gzip is available on the remote, falling back to a plain POST when it isn't, so a box with only curl + bash still works.

A poll answered 410 (session gone) or 426 (hostname retired) ends the runner with a message, because neither can be fixed by retrying. Every other poll failure is retried with a widening wait, 2s doubling up to 30s and reset by the next answered poll, so a relay that is down is not polled - and its error line not reprinted - every two seconds for as long as the listener is left running.

GET /api/health

Returns {"ok": true, "service": "<DOMAIN>"}, so a monitor can also confirm which relay answered.

Requests arriving on a retired hostname

A name that still resolves here but is not DOMAIN is answered 426 Upgrade Required on every route, carrying the current endpoint and the fix. Nothing is served on the old name; clients fail loudly until they are updated.

POST /api/session and GET /api/session/{key} are the exception: they answer 200 with a session payload whose quickstart lines and mcp_messages templates carry the notice in place of a command. Clients render those and discard error bodies, so it is the only way the message reaches an operator. The payload is dead by construction - no session exists behind it, and its URLs use a scheme no HTTP client can open - so nothing runs.

Skipped entirely when DOMAIN=localhost, so a local stack reached over 127.0.0.1 keeps working.

This includes health probes: an uptime monitor that hits the relay by IP or by a hostname other than DOMAIN sees 426, not 200. Point monitors at GET https://DOMAIN/api/health (or send a Host: DOMAIN header).

Landing page

GET / serves a tiny zero-dependency HTML page that calls /api/session, shows the generated key, the remote-side one-liner with a copy button, and the curl-only exec template you can paste straight into any terminal. Useful when an operator just needs the one-liner without hitting the API manually.

Using it from an LLM tool

Two integration paths. Pick whichever your tool supports:

  • MCP (most coding agents): your tool runs mcp/server.js locally and the model gets a native remote_exec tool.

  • HTTP / curl (any tool with a Bash/shell tool): drop templates/AGENTS.md.example into your repo as the rules file your tool reads; the model calls the API itself with curl.

The MCP server is on npm as remotify-mcp - every host below can use npx to run it with zero install.

Claude Code

claude mcp add -s user remotify -- npx -y remotify-mcp@latest

-s user registers the server globally (in ~/.claude.json at the user scope) so it's available from every working directory. Without -s user it lands in project-local scope and only appears in sessions whose cwd matches where you ran the command.

Verify:

claude mcp list          # remotify should be listed

Inside a session, the /mcp slash-command shows the live status and the exposed remote_exec tool.

Cursor

Edit ~/.cursor/mcp.json:

{ "mcpServers": { "remotify": { "command": "npx", "args": ["-y", "remotify-mcp@latest"] } } }

Restart Cursor.

Codex CLI (OpenAI)

Edit ~/.codex/config.toml:

[mcp_servers.remotify]
command = "npx"
args    = ["-y", "remotify-mcp@latest"]

OpenCode V2

opencode mcp add remotify --global -- npx -y remotify-mcp@latest

Requires Node.js 18+ with npx. --global registers the server for every project in ~/.config/opencode/opencode.jsonc; omit it for the current project only. No account, API key, or global npm install is needed.

Check the connection with opencode mcp list or /mcps inside OpenCode. Ask OpenCode to call remote_session_info and show both connection one-liners, then paste your chosen line on the remote. Supervised mode asks for approval before each command; auto mode executes without approval.

Gemini CLI

Edit ~/.gemini/settings.json:

{ "mcpServers": { "remotify": { "command": "npx", "args": ["-y", "remotify-mcp@latest"] } } }

Any other MCP-capable tool

Windsurf, Continue, Cline/Roo Code, Zed, VS Code's built-in MCP - they all accept the same command + args shape, just in a config file they each document. Drop the snippet in and you're done.

Tools that are NOT MCP-capable (ChatGPT web, Ollama UIs, any bash-only agent, ...)

Drop templates/AGENTS.md.example into your project as the rules file the tool reads - common filenames: AGENTS.md, CLAUDE.md, .cursorrules, CONVENTIONS.md, .github/copilot-instructions.md. It teaches the model to POST /api/session and use curl to push/pull. No server-side integration needed.

Overriding the relay

All snippets above default to https://relay.remotify.run. If you self-host, add one env var:

"env": { "REMOTIFY_URL": "https://remotify.example.com" }

Using it from anything else

If it speaks HTTP, it can talk to the relay. The only API call needed to get started is POST /api/session; everything after that is an HTTP GET or POST against DOMAIN - there is no separate pipe. subdomain, it's all one vhost.

Security model

  • Key = access. 128-bit random hex, unguessable at internet scale. Keep it private; anyone with the key can push commands to any listener on that key.

  • TLS is mandatory in production. Keys travel in URL paths; HTTPS prevents interception.

  • nginx rate-limits POST /api/session per IP (see RATE_LIMIT).

  • Data-minimised persistence. Each session's pending command/result and push history live in a 0700 directory on the relay host, readable only by the PHP worker user. Kept only as long as needed: purged on the idle TTL (default 3h) or an explicit DELETE.

  • Supervised mode on the remote. /r/KEY?mode=supervised prompts y/N before running each incoming command.

  • --data-raw (rather than plain -d / --data) is used throughout to avoid curl's @file magic - a command that starts with @ will not be read as a filename.

Threat-wise this is roughly equivalent to handing someone an SSH session: commands run as whatever user pasted the remote one-liner. Do not paste the one-liner on a shell you would not SSH into.

License

Apache-2.0. See LICENSE and NOTICE.

Available Tools

3 tools
remote_execA

Execute a shell command on the PROJECT'S REMOTE SERVER via the remotify relay (the deployed/production/staging box, NOT the developer's local machine). Examples: 'check disk usage', 'restart the service', 'tail the app log', 'query the prod DB', 'is port 443 open in prod', 'what version is running in staging'. Chain this tool freely for iterative work - log analysis with grep/awk/sed/head/tail, narrowing a search, cross-checking outputs, diagnosing slow services with ps/vmstat/netstat/iostat. Each call saves a manual copy-paste roundtrip; only surface the meaningful result or synthesis to the user. In supervised mode the operator approves each command on the remote via y/N prompt, so no extra confirmation is needed from the assistant. Returns combined stdout+stderr as plain text on success.

HARD RULE: the user connects the remote by pasting a listener one-liner; that is the ONLY execution path. NEVER fall back to ssh, scp, or any other transport, and NEVER try to start the listener yourself. The user chose remotify precisely so that no agent gets shell access.

Pending handling: the response text may start with one of these markers. '[pending]' WITH paste one-liners = no remote listener is connected; nothing can run until the user pastes one. STOP: end your turn immediately with a short message that shows BOTH one-liners (supervised and auto) verbatim in a fenced code block. Do not call any tool after that message: hosts collapse text written between tool calls, so the one-liners only reach the user as the final message of the turn. The command stays queued; when the user replies that the listener is running, call remote_exec again with the EXACT SAME arguments to collect the output. It will not execute twice. '[pending]' WITHOUT one-liners = the listener is online but the relay cannot confirm pickup yet. Call remote_exec again with the EXACT SAME arguments; loop silently, do not ask the user anything, and give up only after about 10 consecutive [pending] responses. '[in-flight]' = the command IS already being executed on the remote (cmd was picked up, no output yet). In that case: 1. Do NOT give up. Long operations (mongodump, restic, rsync, big installs, slow restarts) routinely take many minutes. 2. Call remote_exec again with the EXACT SAME arguments; the relay will return the output the moment the command finishes. 3. Do NOT tell the user the listener disconnected; do NOT ask them to re-paste the listener; do NOT switch to a different command. 4. There is no LLM-side retry cap on '[in-flight]'; keep waiting until the result lands or the relay returns something else. '[busy]' = you tried to issue a NEW command while a previous one is still executing on the remote. Each session is single-flight (one command at a time). In that case: 1. If you know the previous command, call remote_exec with ITS exact arguments first to drain its result, then issue the new one. 2. If you do NOT know it (e.g. it was issued by an earlier session), keep calling remote_exec with the NEW command's arguments: stale state left by a dead session is cleared automatically (see '[recovered]'). 3. If the [busy] response contains paste one-liners, the listener is disconnected and the wedge can ONLY clear once the user reconnects it: end your turn with BOTH one-liners verbatim, exactly as for a no-listener [pending]. '[recovered]' = wedged state left by a dead session/listener was cleared automatically. Read the rest of the message: either the command already proceeded, or you are asked to call remote_exec again with the EXACT SAME arguments. '[resumed]' = the returned output was already waiting on the relay and belongs to the most recent run (e.g. you re-issued a command after an interruption). Use it if it matches what you expected; if not, issue the command again. '[unknown-result]' = the session expired while the command was already running on the remote. Its output was lost and it may or may not have completed. A new session was minted; end your turn with BOTH reconnect one-liners verbatim, then decide whether re-running is safe once the user is back. The server will NOT silently re-run it. '[session-renewed]' = the session key changed; the one-liners that follow are for the NEW key. Treat it like a no-listener [pending]: end your turn with both lines verbatim.

ParametersJSON Schema
NameRequiredDescriptionDefault
commandYesShell command to execute on the remote machine

TDQS

A4.2/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations the description carries the full behavioral burden, and it does disclose unusually rich context: combined stdout+stderr return format, supervised y/N approval, and detailed semantics for every status marker ([pending], [in-flight], [busy], [recovered], [resumed], [unknown-result], [session-renewed]). This is far beyond what a bare schema conveys and earns more than baseline, but much of it is protocol/state-machine guidance rather than core tool behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Content is front-loaded with purpose before the marker-handling detail, and sections are clearly delineated. It is nonetheless very long for a single-parameter tool, with some repetitive reinforcement (e.g. the repeated instruction to re-issue the EXACT SAME arguments and to emit both one-liners verbatim), which slightly dilutes the otherwise disciplined structure.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, so the description correctly explains the return value (combined stdout+stderr as plain text). Combined with the exhaustive marker-handling state machine and the reconnect fallback rules, an agent has everything needed to drive this tool through its failure modes without guessing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and there is only one parameter, so the schema already documents 'command' fully. The description's examples imply what kind of commands are appropriate but add no syntax or format detail beyond the schema, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ('Execute a shell command') and resource ('on the PROJECT'S REMOTE SERVER via the remotify relay'), and explicitly disambiguates against the developer's local machine. The concrete examples ('check disk usage', 'restart the service', 'tail the app log') pin down the intended scope, letting an agent distinguish it from remote_session_info and remote_session_reset without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use guidance ('Chain this tool freely for iterative work - log analysis with grep/awk/sed...') and a HARD RULE for when-not-to-use alternatives ('NEVER fall back to ssh, scp, or any other transport'). It also explains the supervised-mode approval flow so the agent knows no extra confirmation is needed.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

remote_session_infoA

Return the current relay session: key, URLs, and the exact one-liners to paste on the remote machine to start the listener (remote_quickstart = supervised, y/N per command; remote_quickstart_auto = unattended). When you show them to the user, show BOTH verbatim in a fenced code block and make them the final message of your turn.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden, and it delivers useful non-schema behavior: the session is 'current' (live relay state), the supervised variant prompts y/N per command while the auto variant is unattended, and there is a hard presentation contract (show both one-liners verbatim in a fenced code block as the final message of the turn). It stops short of stating whether calling it mutates or rotates session state.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose, then the field contents, then the presentation directive. It is dense and the first sentence is run-on with parentheticals, but every clause carries information the agent needs and nothing is redundant.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, so the description must describe returns, and it does: key, URLs, and both listener one-liners with their execution modes. Combined with the explicit formatting instruction for surfacing the result to the user, an agent has everything needed to call it and present it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Zero parameters, so the baseline is 4. The description goes slightly further by explaining the semantics of the returned fields (the two quickstart one-liner variants and their supervision modes), which is meaning the empty schema cannot convey.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Return the current relay session') and enumerates exactly what comes back: key, URLs, and the paste-ready listener one-liners. The scope is distinct from remote_exec (runs commands) and remote_session_reset (tears the session down), so an agent can route correctly without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: an agent infers it should call this to obtain session details and bootstrap commands before acting remotely. The description does clarify the two output variants (remote_quickstart = supervised with y/N per command, remote_quickstart_auto = unattended), which helps choose which one-liner to hand the user, but there is no explicit when-to-use or when-not statement relative to the siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

remote_session_resetA

Recover a wedged remotify session. Default (no arguments): clear stuck relay state in place - drops any queued command/result and the in-flight marker; the session key and a connected listener keep working. With {"rotate": true}: abandon the current session and mint a brand-new key - the old key dies, the remote listener disconnects, and the user must paste the NEW runner one-liner (returned by this tool) on the remote. Normally NOT needed: remote_exec auto-recovers from stale state by itself. Use it only when remote_exec stays wedged (repeated [busy] for a command you never issued) despite same-args retries, or when the user explicitly asks to reset or rotate the session.

ParametersJSON Schema
NameRequiredDescriptionDefault
rotateNoMint a brand-new session key instead of clearing the current one in place (requires the user to reconnect the remote listener)

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does so well: it discloses that the default path drops queued command/result data and the in-flight marker while preserving the session key and listener, and that rotate=true kills the old key, disconnects the listener, and forces the user to paste a new one-liner. It also notes the tool returns the new runner one-liner.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the default behavior, then the rotate branch, then the when-to-use guidance. It is dense but every clause earns its place by describing a distinct behavioral consequence or selection condition; there is no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-optional-param recovery tool with no annotations and no output schema, the description supplies everything needed: default vs rotate semantics, destructive consequences, the returned value, and the exact conditions that justify invocation. Nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3, but the description goes beyond the schema by contrasting the default no-argument behavior with rotate=true and spelling out the user-visible consequences of rotation (key dies, listener disconnects, reconnect required). That is meaningful added semantics rather than a restatement.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource ('Recover a wedged remotify session') and immediately distinguishes itself from the sibling remote_exec by explaining that remote_exec normally auto-recovers. An agent can tell exactly what this tool is for and how it differs from the other two siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when NOT to use it ('Normally NOT needed: remote_exec auto-recovers from stale state by itself') and the precise conditions that select it (repeated [busy] for a command never issued despite same-args retries, or an explicit user request to reset/rotate). It names the alternative tool and the triggering symptom.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updates
    • First observedremote_exec
    • First observedremote_session_info
    • First observedremote_session_reset

TDQS

A4.1/5.0

Scored across 3 tools

Disambiguation4/5

The three tools target distinct intents: read session state (remote_session_info), run a command (remote_exec), and recover/rotate a session (remote_session_reset). There is some overlap because remote_exec's description absorbs most of the state-handling (pending/in-flight/busy/recovered) logic that conceptually belongs with remote_session_reset, so the boundary between 'just retry exec' and 'call reset' can be blurred. Still, an agent can tell the three apart.

Naming Consistency4/5

remote_session_info and remote_session_reset follow a clean remote_session_<verb> pattern, while remote_exec shortens to remote_<verb>. All snake_case with a consistent remote_ prefix, so it reads predictably, but the session/exec split is a minor deviation rather than a fully uniform scheme.

Tool Count4/5

Three tools is lean but appropriate for a deliberately minimal, security-bounded relay surface (info, execute, reset). Each tool earns its place, though the low count means a lot of lifecycle complexity is crammed into remote_exec's description instead of being expressed as tools.

Completeness3/5

The core lifecycle (connect/info, run, recover/rotate) is covered, but there is no way to cancel or interrupt an in-flight command, no separate status/output-poll tool, and no explicit teardown beyond reset. These gaps are workable via the marker protocol, but agents have no clean primitive for interrupting long operations other than dropping the whole session.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    F
    maintenance
    An MCP server that allows AI models to execute system commands on local machines or remote hosts via SSH, supporting persistent sessions and environment variables.
    1
    18 npm
    28
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    An MCP server that gives AI agents SSH access to remote machines through your local OpenSSH client, enabling remote command execution, file transfer, persistent shell sessions, and port forwarding.
    17
    272 PyPI
    30
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A security-first MCP server that gives AI assistants controlled, safe access to manage remote servers via SSH with whitelisted operations and no generic command execution.
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A secure MCP server that enables AI assistants to execute commands on remote servers via SSH, with cross-platform support and features like encrypted config storage, connection management, and security controls.
    1
    MIT