remote-agent-connector
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-agent-connectorRun 'df -h' on remote agent device prod-01 over SSH."
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 Agent Connector
Typed Remote Agent connector for Business MCP Hub using the VeilBrowser Fleet pattern:
Business MCP Hub -> remote-agent-mcp-connector (/mcp)
-> remote-agent-wss-relay (/relay)
-> client Remote Agent ConnectorThe connector service exposes 23 typed tools through Streamable HTTP MCP. It
also has a dedicated /relay endpoint for device WebSocket connections and a
SQLite device registry for capabilities, health, connection state, and
revocation.
Every tool handler is asynchronous and awaits the inner device command before returning a JSON-ready object. MCP clients therefore receive a normal result or MCP error envelope, never an unresolved coroutine.
Required environment variables
REMOTE_AGENT_DATABASE_URL=postgres://... or sqlite:///...
REMOTE_AGENT_MCP_BEARER_TOKEN=...
REMOTE_AGENT_HUB_DELEGATION_SECRET=...
REMOTE_AGENT_OPERATOR_BEARER_TOKEN=...
REMOTE_AGENT_HUB_AUDIENCE=remote-agent-connector
REMOTE_AGENT_PRIVATE_MCP_URL=http://remote-agent-mcp-connector:3030/mcp
REMOTE_AGENT_PUBLIC_RELAY_URL=wss://10.21.4.101:3051/relay
REMOTE_AGENT_BIND_HOST=0.0.0.0
REMOTE_AGENT_BIND_PORT=3030
REMOTE_AGENT_ALLOWED_HOSTS=remote-agent-mcp-connector:3030,127.0.0.1:3030,localhost:3030,10.21.4.101:3051For local development only, set
REMOTE_AGENT_ALLOW_SQLITE_DEV=1, REMOTE_AGENT_ALLOW_INSECURE_HTTP=1, and
REMOTE_AGENT_ALLOW_INSECURE_HTTP=1.
Optional:
REMOTE_AGENT_REQUIRE_SIGNED_ASSERTION=0
REMOTE_AGENT_REQUEST_TIMEOUT_SECONDS=120 # 1-600
REMOTE_AGENT_RELAY_HANDSHAKE_TIMEOUT_SECONDS=15 # 1-60Unset or 0 keeps Phase 2 in shadow mode: a v4 caller whose Hub has no signing
material still reaches the device with the Phase 1 statement object. Set 1
only after the Hub publishes key material and the Windows lane verifies
cryptographically; then an unsigned v4 call fails closed.
Related MCP server: AnythingLLM MCP Server
Two clocks
REMOTE_AGENT_REQUEST_TIMEOUT_SECONDS is the relay wait: how long the
connector holds a device call open before it reports device_timeout. It is
bounded at 600 seconds, and the only thing it has to fit inside is the Hub's
upstream read timeout. If the Hub gives up first, it returns a failure while the
device keeps executing the command, which is worse than a short ceiling.
The signed app assertion is a separate budget. A device judges freshness when the request frame arrives, not when it produces the response, so the assertion only has to outlive delivery from Hub to device. That is why the Hub can keep a 180 second assertion lifetime while the connector waits far longer for a slow command. The two numbers were previously tied together, which capped legitimate long commands at the assertion lifetime for no security gain.
The relay waits at most REMOTE_AGENT_RELAY_HANDSHAKE_TIMEOUT_SECONDS (15
seconds by default) for the first enrollment/authentication frame. This bound
does not apply to heartbeats or command responses. A client that reaches the
Python relay receives a fixed diagnostic with error_contract, code, stage,
retryable, and message; the payload never includes exception text, tokens,
public keys, signatures, challenge values, request bodies, credentials, or
filesystem paths. The stable contract is:
relay_tls_failed # local/client-side mapping; no relay frame exists
relay_upgrade_failed
relay_challenge_timeout
relay_challenge_invalid
enrollment_token_invalid
enrollment_token_expired
enrollment_token_consumed
connector_id_mismatch
enrollment_signature_invalid
enrollment_challenge_invalid
device_already_enrolled
device_revoked
relay_authentication_failed
relay_ready_failed
relay_protocol_errorAn HTTP WebSocket upgrade or reverse-proxy failure happens before the Python
handler can send a relay frame. The baked Nginx front returns the bounded
relay_upgrade_failed JSON diagnostic instead. Transport TLS remains strict;
the timeout is not a TLS bypass.
The canonical error vocabulary is published in
contracts/remote-agent-relay-errors-v1.json. Python relay frames carry the
same error_contract identifier and the Nginx upgrade response carries it as
well. relay_tls_failed is transport-only: a certificate failure occurs
before HTTP/WebSocket application data exists, so the client or operator must
surface that code locally rather than expecting a relay error frame.
The operator can read the same closed vocabulary at
GET /operator/relay-errors/v1 with the operator bearer.
Operator CA publication
Set the optional path below to a public CA certificate mounted read-only into
the connector. The file must contain exactly one CA certificate with
BasicConstraints CA=TRUE; it must not contain a private key:
REMOTE_AGENT_PUBLIC_CA_CERT_PATH=/etc/remote-agent/ca/remote-agent-ca.crtAn operator can read the versioned publication route with the existing operator bearer:
GET /operator/relay-ca/v1The response contains only the normalized public certificate, SHA-256
fingerprint, subject/issuer and validity metadata. If the path is unset,
missing or invalid, the route returns a bounded 503 code:
relay_ca_artifact_unconfigured, relay_ca_artifact_unavailable or
relay_ca_artifact_invalid. The route is publication-only: neither the
connector nor a client auto-trusts a certificate fetched from it. Verify the
fingerprint out of band and install the CA explicitly on approved clients.
Relay certificate issuance
This service is also the CA signer. Point it at the matching CA private key and the address the relay actually serves:
REMOTE_AGENT_CA_PRIVATE_KEY_PATH=/etc/remote-agent/ca/remote-agent-ca.key
REMOTE_AGENT_RELAY_IP=10.21.4.101
REMOTE_AGENT_RELAY_CERT_VALIDITY_DAYS=90With the key unset, publication keeps working and issuance answers
relay_ca_signer_unconfigured. The CA key is never returned by any route and
never leaves this container; the relay host and every client hold public
material only.
POST /operator/relay-certificates/v1
Authorization: Bearer <REMOTE_AGENT_OPERATOR_BEARER_TOKEN>
{"csr_pem": "<PEM-encoded certificate signing request>"}The signer refuses a CSR unless it is exactly one IP: SAN matching
REMOTE_AGENT_RELAY_IP, carries CA=FALSE and serverAuth, has no
clientAuth, and uses a key at least 2048-bit RSA or 256-bit EC. Issued leaves
are capped at 397 days. The Business MCP Hub exposes the same operation as
POST /admin/api/remote-agent/relay-certificates for the renewal runner.
scripts/renew_relay_certificate.py is that runner. It generates the leaf key
and CSR on the relay host, requests a certificate, verifies the answer against
the published CA, backs up the current pair, swaps atomically, reloads nginx,
and re-checks the live handshake. It restores the backup if anything after the
swap fails. Run it from cron; it does nothing until the leaf is inside
--renew-before-days.
Two guards keep the separation honest:
The connector refuses to send a frame whose assertion is already expired, as
app_assertion_expired_at_dispatch. Delivering it would only earn a deny.A per-call
timeout_slarger than the relay wait is refused astimeout_exceeds_relay_window, so a caller can never ask for a command the transport would abandon mid-run.
The device applies its own default when timeout_s is absent, so a caller that
needs a long command has to say so explicitly.
In-flight collision guard
Transport retries must preserve the same idempotency_key. A same-key retry
with the same intent replays the first result, while the same key with a
different intent is refused as idempotency_conflict. A newly generated key is
a new operation, not a retry of the original operation.
The connector therefore holds a second guard keyed on the request content
instead: (connector_id, tool, canonical digest of arguments), independent of
the caller's idempotency key. The window runs from dispatch until a terminal
response, a cancel, or the relay timeout. A second identical call inside that
window is refused with command_in_flight.
Guarded tools are the mutating ones: files.write, files.delete,
files.move, files.mkdir, files.upload, terminal.execute, ssh.execute,
skills.materialize, skills.execute, mcp.call. Read verbs are never
guarded, and nothing here dedups by arguments across time, so git status
before and after an edit both really run.
command_in_flight is a busy signal, not a permission failure and not a device
failure. It is safe to retry once the first call settles.
The connector forwards the existing key as the relay frame's top-level
idempotency_key field. It is not added to arguments, so the device receives
the same business argument shape and can apply its own replay protection before
dispatching a mutation.
Cancel
POST /operator/requests/{connector_id}/{request_id}/cancel sends one cancel
frame over the device's existing relay WebSocket. It names a single request_id
and carries no arguments, no assertion and no authority, so it cannot reach
anything the original request did not already reach.
The connector resolves its own waiting caller with cancelled immediately and
frees that request's collision slot. A device that has not implemented cancel
ignores the frame, so this is safe to deploy before the Windows lane supports
it. The full wire contract lives in
.docs/contracts/remote-agent-relay-control-v1.json.
Tool catalog
connector_health
files_list
files_stat
files_search
files_read
files_write
files_delete
files_move
files_mkdir
files_upload
files_download
terminal_execute
terminal_stream
ssh_execute
ssh_list_profiles
skills_list
skills_materialize
skills_execute
mcp_list_servers
mcp_call
mcp_health
skills_health
connector_restart_mcpThe Hub exposes these as agy_connector__<tool_name>, which matches the
agy_connector.* tool contract used by agy2api. The connector id, audience,
service name, and environment variables stay remote-agent /
REMOTE_AGENT_*; only the MCP namespace carries the consumer-facing prefix.
Authenticated agent status
The connector exposes an operator-only status route:
GET http://remote-agent-mcp-connector:3030/agents
Authorization: Bearer <REMOTE_AGENT_OPERATOR_BEARER_TOKEN>It returns only enrolled devices with a fresh online relay instance. Stale
instances are marked offline before the response. Each entry is sanitized to
device_id, platform, capabilities, health, and
connected_at; public keys, enrollment tokens, relay signatures, and
filesystem paths are never returned.
The private operator device inventory additionally returns the redacted
public_key_fingerprint, capability profile, enrollment state, and heartbeat
timestamps for Admin device management. It never returns the public key
material itself or any enrollment secret.
For all 23 MCP tools, agy2api passes the selected device_id as
profile_id. The connector resolves that value as the exact enrolled device
and optionally accepts instance_id to pin one live process. The capability
tier (read_only, read_write, or full_agent) is separate from
profile_id.
The public WSS endpoint is separate from MCP:
wss://10.21.4.101:3051/relayMCP remains authenticated Streamable HTTP at /mcp; WSS is only for device
relay traffic and is not a generic MCP-over-WebSocket transport.
Images
publish-ghcr.yml builds and pushes both production images to GHCR:
ghcr.io/ngojclee/remote-agent-connector:latest # /mcp connector
ghcr.io/ngojclee/remote-agent-connector:relay-latest # TLS WSS relay frontThe relay image bakes deploy/remote-agent-relay.nginx.conf; only the TLS
certificate and key are mounted at runtime. Migrations install inside the
Python package, so no build tree needs to be bind-mounted into the container.
Windows pairing and revocation
Pairing is operator-controlled and one-time:
The operator calls
POST /operator/enrollment-tokenswithconnector_id,capability_profile(read_only,read_write, orfull_agent),display_label, and a shortexpires_in_secondsvalue.The Windows connector opens the outbound WSS
wss://10.21.4.101:3051/relay. The server sends a challenge. The client generates its Ed25519 key locally and sends the signedenrollpayload containing the one-time enrollment token and its public key.The server consumes both token and challenge, stores only the public-key identity and the approved capability profile, then issues a second challenge for normal authenticated relay operation.
The client signs the second challenge, receives
ready, and sends heartbeats. The MCP connector routes every tool call by the exactprofile_idsupplied by agy2api, which is the enrolled device id.
Enrollment tokens, private keys, signatures, and operator bearer values are
never returned by GET /agents and must not be placed in logs or source
control. To revoke a device, the operator calls
POST /operator/devices/{device_id}/revoke; the connector immediately marks
the device revoked and closes its live instance. A revoked record may be
purged separately with DELETE /operator/devices/{device_id} after
revocation has been verified.
Signed app assertions (Phase 2)
The Business MCP Hub is the only issuer of an application identity statement.
It signs a short-lived Ed25519 assertion per device call and sends it as
X-MCP-Hub-App-Assertion. The connector never holds the signing root, so it
cannot forge or edit the statement; it only:
parses the envelope strictly,
requires
client_id,app_id,scopesandnonceto equal the HMAC identity it already verified,requires
connector_idto equal the call'sprofile_idtarget,uses the envelope's
request_idas the relayrequest_id, so the assertion is bound to exactly one frame,forwards the envelope verbatim inside
app_assertion.
remote_agent_connector/app_assertion.py is the reference device-side
verifier and the executable form of the contract: pinned root, keyset
verification, key-state and expiry checks, binding comparison, and replay
rejection. The Windows connector implements the same rules in Rust.
The shared byte-level vector lives in tests/fixtures/app_assertion_vector.json
and mirrors .docs/contracts/app-identity-v2.json in the Hub repository. Change
one and the other suite fails. Those keys are test-only and must never be used
as live signing material.
v3 callers, and v4 callers whose Hub has no signing material while the require
flag is unset, keep the application identity fields omitted. Every device call
still carries its existing idempotency_key as top-level relay metadata.
Device liveness
Whether a device is online is derived from the age of its last heartbeat, not
from the stored state column. A row left behind by a crash, a container
restart, or a dropped relay therefore cannot be reported as live or routed to.
The window is five heartbeat intervals, so 75 seconds of silence at the current
15 second cadence. That is deliberately wider than
REMOTE_AGENT_HEARTBEAT_TIMEOUT_SECONDS, which is the transport timeout rather
than a liveness verdict: one lost packet or a short blip must not retire a
healthy session. If the configured timeout is larger than five intervals, the
configured value wins.
A sweep closes overdue rows by setting state = 'offline' and
disconnected_at to the last heartbeat, never to the sweep time, so a reaped
row stays distinguishable from a clean disconnect and never claims a device
outlived its silence. The sweep runs on every inventory and routing read.
This server cannot be deployed
Maintenance
Related MCP Connectors
Remote MCP server to read and manage your Atako AI agents, messages, files, and integrations.
Build and run grounded business agents over MCP: agents, knowledge bases, skills, Storylines.
Agent-native collaboration network: orchestrate a team of long-running agents from any MCP client.
Agent communication platform for agent to agent messaging via MCP. Messages, channels, skills.
Related MCP Servers
- FlicenseNot gradedqualityAmaintenanceEnables MCP-compatible AI agents to execute typed live-app actions over WebSocket, allowing agents to directly interact with real application state without browser automation or scraping.21-
- AlicenseAqualityCmaintenanceEnables MCP-compatible clients to interact with AnythingLLM, providing tools for workspace management, chat and thread operations, document operations, vector search, and system inspection.346MIT
- FlicenseNot gradedqualityCmaintenanceEnables agents to connect to remote MCP servers once, access their tools through a compact MCP endpoint, pair a CLI inside sandboxes, and create watches that turn command or tool output into pollable structured events.2-
- AlicenseNot gradedqualityBmaintenanceEnables agent clients to safely connect to tools and execution resources through MCP with authorization, approvals, audit, chat-context isolation, SSH/Docker access, and long-running command session tracking.MIT