Host a remote MCP server
Tools for all your agents, on a computer of their own. Private until you say.
Shelley handles the setup. About 10 minutes.
Prefer a terminal?
curl -fsSL https://exe-staging.dev/deploy/mcp-server/prompt.txt | ssh exe.dev new --prompt=/dev/stdinAn MCP server gives AI agents tools: search your notes, query your database, run your tests. On your laptop, only local agents can use it; at an HTTPS URL, all can: Claude Code and Codex on your computer, agents elsewhere, claude.ai and ChatGPT on your phone.
On an exe.dev VM it stays private: agents send a token in one header, and your server learns who's calling with no auth code. For claude.ai and ChatGPT, which require OAuth, a small gate signs people in with exe.dev. The VM stays on and keeps its disk, so tools can keep files and databases, check out repositories, and run minutes-long jobs, and its agent adds tools on request. Start from a notes server, bring your own, or wrap stdio-only servers.
What you get#
- An MCP server on Streamable HTTP at
https://my-mcp-server.exe.xyz/mcp(/servers/<name>/mcpfor stdio servers) asmcp-server.service, restarted on failure and reboot: the starter, your repository, or stdio servers behind mcp-proxy. - Private behind exe.dev: clients send a VM token in one header, and your server reads the caller from
X-ExeDev-Emailwith no auth code. ~/bin/mcp-check, a small client that lists the tools and calls one, run against the server as exe.dev's proxy reaches it.- Code,
~/data, and settings backed up nightly to~/backups, with a restore checked in a scratch directory.
What's running, and where
https://my-mcp-server.exe.xyz/mcp
the MCP endpoint, for you and agents with a VM token
http://notes.int.exe.xyz/mcp
the same, for agents on your other VMs (VM-to-VM integration)
https://my-mcp-server.exe.xyz/mcp
with mcp-gate only: OAuth sign-in for claude.ai, ChatGPT, and their apps
mcp-server.service
Deploy with Shelley#
Click Deploy and Shelley will set up Remote MCP server on a new VM, then check that it works. No config-file wrangling required.
Have these ready; Shelley asks for them:
SERVER:newfor the notes starter; a GitHub repository (owner/name or URL) with an MCP server; or a stdio server's command (npx -y @modelcontextprotocol/server-filesystem@2 ~/shared) or Claude Desktop config
Prefer a terminal?
curl -fsSL \
https://exe-staging.dev/deploy/mcp-server/prompt.txt \
| ssh exe.dev new --prompt=/dev/stdinWhen it's done, run this on your computer (only you can):
ssh exe.dev ssh-key generate-api-key --vm=my-mcp-server --label=mcp-clients --exp=90d
claude mcp add --transport http --scope user notes https://my-mcp-server.exe.xyz/mcp --header "X-Exedev-Authorization: Bearer exe1.paste-yours-here" # or the name and URL Shelley gave youWhat Shelley is told
SERVER=<fill-this-in>
Set up Remote MCP server on this VM by following the exe.dev recipe at https://exe-staging.dev/deploy/mcp-server.md from top to bottom.
The values at the top of this message are mine:
- SERVER: `new` for the notes starter; a GitHub repository (owner/name or URL) with an MCP server; or a stdio server's command (`npx -y @modelcontextprotocol/server-filesystem@2 ~/shared`) or Claude Desktop config
Ask me for any that still say <fill-this-in> before you need them.
The recipe's example VM name is my-mcp-server. This VM's name is the output of `hostname`: use it wherever the recipe writes my-mcp-server. Skip the step that creates the VM; you are on it.
You are done when all of these are true. Check each one yourself:
- An MCP server on Streamable HTTP at `https://my-mcp-server.exe.xyz/mcp` (`/servers/<name>/mcp` for stdio servers) as `mcp-server.service`, restarted on failure and reboot: the starter, your repository, or stdio servers behind mcp-proxy.
- Private behind exe.dev: clients send a VM token in one header, and your server reads the caller from `X-ExeDev-Email` with no auth code.
- `~/bin/mcp-check`, a small client that lists the tools and calls one, run against the server as exe.dev's proxy reaches it.
- Code, `~/data`, and settings backed up nightly to `~/backups`, with a restore checked in a scratch directory.
Read the whole recipe, closed details included, and follow "Set up by
hand" in order, skipping VM creation; the walkthrough above it is for
the owner. Ask for SERVER first if it says <fill-this-in>. For `new`,
build the starter exactly as written; for a repository, follow "Or: a
server from a repository"; for a stdio server or Claude Desktop config,
"Or: stdio servers". Ask the owner for any secret a server needs and put
it in ~/mcp-server.env, never in servers.json or the unit. If a private
repository won't clone, give the owner the `integrations add github`
command with this VM's name and wait.
Set up mcp-gate only if the owner asks for claude.ai, ChatGPT, or their
apps, following "Connect claude.ai and ChatGPT" on the VM; don't make
the VM public, but give the owner the `share set-public` command.
Delete notes you save while checking. Don't reboot or restore over live
data: test the backup in a scratch directory as written. The VM can't
open its own https://<vm>.exe.xyz; "Check it" goes through localhost.
End with each MCP URL, a short name and the tools for each, and
anything you adapted or the owner should change.
Finish with the URLs I should open and these commands, which only I can run, from my own computer (with this VM's name in them):
ssh exe.dev ssh-key generate-api-key --vm=my-mcp-server --label=mcp-clients --exp=90d
claude mcp add --transport http --scope user notes https://my-mcp-server.exe.xyz/mcp --header "X-Exedev-Authorization: Bearer exe1.paste-yours-here" # or the name and URL Shelley gave you
Also at /deploy/mcp-server/prompt.txt. The recipe it follows is /deploy/mcp-server.md.
Connect your agents#
Agents can't sign in with a browser, so make them a VM token on your
computer (--exp sets its expiry):
ssh exe.dev ssh-key generate-api-key --vm=my-mcp-server --label=mcp-clients --exp=90d
export EXE_TOKEN=exe1.paste-yours-here
Agents send X-Exedev-Authorization: Bearer <token>. exe.dev checks and
strips it, and forwards the request with X-ExeDev-Email set to your
address; requests without it never reach your server
(HTTPS tokens for VMs).
check.py connects like an agent, lists the tools, and can call one:
uv run https://exe-staging.dev/deploy/mcp-server/check.py https://my-mcp-server.exe.xyz/mcp \
-H "X-Exedev-Authorization: Bearer $EXE_TOKEN"
connected: notes (protocol 2026-07-28)
4 tools: save_note, search_notes, recent_notes, delete_note
Examples use the starter's name, notes; for your own server, use the
name and URL Shelley gave you.
Claude Code (saves the token in ~/.claude.json):
claude mcp add --transport http --scope user notes https://my-mcp-server.exe.xyz/mcp \
--header "X-Exedev-Authorization: Bearer $EXE_TOKEN"
Codex, in ~/.codex/config.toml:
[mcp_servers.notes]
url = "https://my-mcp-server.exe.xyz/mcp"
http_headers = { "X-Exedev-Authorization" = "Bearer exe1.paste-yours-here" }
Cursor, in ~/.cursor/mcp.json (reads the variable at startup):
{
"mcpServers": {
"notes": {
"url": "https://my-mcp-server.exe.xyz/mcp",
"headers": { "X-Exedev-Authorization": "Bearer ${env:EXE_TOKEN}" }
}
}
}
VS Code, in MCP: Open User Configuration (asks for the token once, then keeps it in secret storage):
{
"inputs": [
{ "id": "exe-token", "type": "promptString", "description": "exe.dev VM token", "password": true }
],
"servers": {
"notes": {
"type": "http",
"url": "https://my-mcp-server.exe.xyz/mcp",
"headers": { "X-Exedev-Authorization": "Bearer ${input:exe-token}" }
}
}
}
Other clients: send X-Exedev-Authorization: Bearer exe1.….
From code: the Claude API's
MCP connector
takes the token as authorization_token; OpenAI's Responses API takes it
in the MCP tool's headers:
tools = [{
"type": "mcp",
"server_label": "notes",
"server_url": "https://my-mcp-server.exe.xyz/mcp",
"headers": {"X-Exedev-Authorization": f"Bearer {os.environ['EXE_TOKEN']}"},
"require_approval": "never",
}]
Tip
Make a token per agent (
--label=laptop-claude) to revoke each alone:ssh exe.dev ssh-key list, thenssh exe.dev ssh-key removewith its name or fingerprint.
Try it#
The starter is shared memory for your agents: notes in SQLite with full-text search. Tell Claude Code:
Remember, with the notes tool, that staging deploys go out on Thursdays and need a sign-off from Priya.
Then ask Codex, Cursor, or claude.ai (once connected):
When do staging deploys go out?
It finds the note, stored in ~/data/notes.db with its author: your
address (from X-ExeDev-Email) or the calling VM.
Add your own tools#
A tool is a Python function with a docstring, like the four in the starter's server.py:
import subprocess
@mcp.tool(annotations=ToolAnnotations(read_only_hint=True))
def disk_usage(path: str = "/home/exedev") -> str:
"""How much disk a directory and its subdirectories use, largest first."""
out = subprocess.run(["du", "-x", "-d1", "-k", path], capture_output=True, text=True).stdout
rows = sorted((line.split("\t") for line in out.splitlines()), key=lambda r: -int(r[0]))
return "\n".join(f"{int(kb) // 1024:>8} MB {p}" for kb, p in rows[:20])
Agents pick tools by docstring and argument types. read_only_hint says
the tool changes nothing, so clients can skip asking; mark deleting tools
destructive_hint=True. After editing ~/mcp-server/server.py, on the
VM:
sudo systemctl restart mcp-server
~/bin/mcp-check http://localhost:8000/mcp
Clients see new tools on reconnect (in Claude Code, /mcp, then
reconnect).
Or ask Shelley, the VM's agent, at https://my-mcp-server.shelley.exe.xyz/.
It can read this page, edit and restart the server, and test the tool:
Add a tool to my MCP server that clones a GitHub repository into ~/repos (or pulls it if it's there), runs its tests, and returns the failures.
Add a tool that takes a URL, opens it in headless Chromium, and returns a screenshot.
That's what a VM is for: tools needing a disk, a browser, or minutes, that would otherwise run on your laptop.
Agents on your other VMs#
Agents on your other exe.dev VMs can connect with no token on disk,
through a VM-to-VM integration. On your
computer, with your agent's VM for my-agent-vm:
ssh exe.dev integrations add http-proxy --name notes \
--target https://my-mcp-server.exe.xyz/ --peer --attach vm:my-agent-vm
Then, on that VM:
claude mcp add --transport http --scope user notes http://notes.int.exe.xyz/mcp
exe.dev adds a key at the network edge, unseen by the agent's VM, and
names the caller in X-Exedev-Source-Vm, which the starter records as
the note's author.
Connect claude.ai and ChatGPT#
claude.ai, ChatGPT, and their apps connect from their makers' servers with OAuth only (no token field), so the server must be public and offer OAuth. mcp-gate.py does it in about 340 lines, with no passwords: people sign in with exe.dev, only addresses you list get in, and each app needs their approval. It takes port 8000 and forwards to your server on 8001. VM tokens for listed addresses and calls from your other VMs still pass, so the clients above keep working.
On the VM, move the server to port 8001 and install mcp-gate, listing who may sign in (exe.dev addresses, comma-separated):
sed -i 's/^PORT=8000$/PORT=8001/' ~/mcp-server.env
sudo systemctl restart mcp-server
mkdir -p ~/mcp-gate && cd ~/mcp-gate
curl -fsSLO https://exe-staging.dev/deploy/mcp-server/mcp-gate.py
uv init --bare --python 3.14 && uv add 'mcp>=2.3,<3'
install -m 600 /dev/null ~/mcp-gate.env
cat > ~/mcp-gate.env <<'EOF'
# Settings for mcp-gate.service. Back this file up.
# exe.dev accounts that may sign in, comma-separated:
ALLOWED_EMAILS=you@example.com
UPSTREAM=http://127.0.0.1:8001
EOF
Run it under systemd:
sudo tee /etc/systemd/system/mcp-gate.service >/dev/null <<'EOF'
[Unit]
Description=mcp-gate: OAuth sign-in with exe.dev in front of the MCP server
After=network-online.target mcp-server.service
Wants=network-online.target
[Service]
User=exedev
WorkingDirectory=/home/exedev/mcp-gate
EnvironmentFile=/home/exedev/mcp-gate.env
ExecStart=/home/exedev/mcp-gate/.venv/bin/python mcp-gate.py
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-gate
Check it. Expect 401 without a sign-in (pointing at its OAuth
metadata), then the tools with an allowed address, a header exe.dev's
proxy sets:
sleep 3
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8000/mcp
~/bin/mcp-check http://localhost:8000/mcp -H "X-ExeDev-Email: $(grep -oP '^ALLOWED_EMAILS=\K[^,]+' ~/mcp-gate.env)"
Now, on your computer, make port 8000 public (other ports stay private):
ssh exe.dev share set-public my-mcp-server
claude.ai: in Settings → Connectors, choose Add custom
connector, enter https://my-mcp-server.exe.xyz/mcp, and leave the
OAuth fields empty. Claude sends you to exe.dev to sign in, then to
mcp-gate to approve. Connectors added on claude.ai also appear in
Claude's desktop and phone apps.
ChatGPT: turn on developer mode and add a custom MCP server with the same URL and OAuth; sign-in and approval work the same.
Sign-ins last 90 days of use, in ~/data/mcp-gate.db. To cut someone
off, remove them from ALLOWED_EMAILS and sudo systemctl restart mcp-gate. To go private again: ssh exe.dev share set-private my-mcp-server.
Warning
The approval page names the app and where it returns you. Approve only right after clicking Connect in that app: anyone can start a sign-in and send you the link.
Bring a server you already have#
Give it to the Deploy button as SERVER, or follow the matching part of
Set up by hand:
- A repository whose server speaks Streamable HTTP, or can (usually a
one-line change). It runs as
mcp-server.serviceinstead of the starter. - A stdio server, the kind you'd run with
npxoruvxfrom a Claude Desktop config. mcp-proxy serves each one you list at/servers/<name>/mcp.
Tokens, VM-to-VM, and mcp-gate work as above.
Warning
A wrapped server runs as
exedev, with that user's access to the VM. The filesystem server reads and writes the directory you give it, for anyone with a token.
The nuts and bolts#
Happy with what Shelley set up? Stop here. Below are the steps it followed, and maintenance for later.
Set up by hand
Create the VM#
On your computer (pick another name if my-mcp-server is taken):
ssh exe.dev new --name=my-mcp-server
ssh my-mcp-server.exe.xyz
Everything else runs on the VM, as exedev.
Prepare the VM#
Install Python 3.14 with the preinstalled uv, make the directories,
create ~/mcp-server.env for settings and secrets, and fetch the client
from Connect your agents as ~/bin/mcp-check (uv
installs its one dependency on first run):
uv python install 3.14
mkdir -p ~/data ~/backups ~/bin
if [ ! -e ~/mcp-server.env ]; then
install -m 600 /dev/null ~/mcp-server.env
cat >> ~/mcp-server.env <<EOF
# Settings and secrets for mcp-server.service.
# One KEY=value per line, no quotes. Back this file up.
PORT=8000
NOTES_DB=$HOME/data/notes.db
EOF
fi
curl -fsSL -o ~/bin/mcp-check https://exe-staging.dev/deploy/mcp-server/check.py
chmod +x ~/bin/mcp-check
Then install one of three: the starter, a repository, or stdio servers.
The starter#
server.py is a 150-line
notes server on the official
MCP Python SDK:
four tools over SQLite, Streamable HTTP at /mcp, stateless so restarts
drop nothing. It answers only requests addressed to $(hostname).exe.xyz
or localhost (421 otherwise) and from no other web page (403), so
sites you visit can't call your tools through your browser.
mkdir -p ~/mcp-server && cd ~/mcp-server
curl -fsSLO https://exe-staging.dev/deploy/mcp-server/server.py
uv init --bare --python 3.14
uv add 'mcp>=2.3,<3'
The service runs it with the project's Python:
sudo tee /etc/systemd/system/mcp-server.service >/dev/null <<'EOF'
[Unit]
Description=MCP server (~/mcp-server)
After=network-online.target
Wants=network-online.target
[Service]
User=exedev
WorkingDirectory=/home/exedev/mcp-server
EnvironmentFile=/home/exedev/mcp-server.env
Environment=PATH=/home/exedev/.local/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/home/exedev/mcp-server/.venv/bin/python server.py
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-server
Now check it.
Or: a server from a repository#
Clone it into ~/mcp-server (your repository for OWNER/NAME) and
install it as its README says:
git clone https://github.com/OWNER/NAME.git ~/mcp-server
If that asks for a password, the repository is private. On your computer, connect it with the GitHub integration, then clone through it, with no token on the VM:
ssh exe.dev integrations add github --name NAME --repository OWNER/NAME --attach vm:my-mcp-server
git clone https://github.int.exe.xyz/OWNER/NAME.git ~/mcp-server
It must speak Streamable HTTP on port 8000, on all interfaces, at a
known path (usually /mcp). Most SDK-based servers can:
- Python SDK (
MCPServer, formerlyFastMCP):mcp.run("streamable-http", host="0.0.0.0", port=int(os.environ["PORT"])). - TypeScript SDK: a
StreamableHTTPServerTransportin an HTTP server onprocess.env.PORT. - Otherwise: look for a
--transport http(orstreamable-http) flag, or wrap it as a stdio server.
If it checks the Host header (both SDKs can, and do when bound to
localhost), allow $(hostname).exe.xyz. Put its secrets in
~/mcp-server.env. Write mcp-server.service as for the starter, with
the server's command (full paths) as ExecStart, and start it.
Or: stdio servers#
A stdio server talks to one client over stdin and stdout.
mcp-proxy starts each one you
list and serves it over HTTP. Install it, plus Node.js 24 from NodeSource
for npx servers:
uv tool install 'mcp-proxy>=0.13,<0.14'
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
List the servers in ~/mcp-server/servers.json, in Claude Desktop's
format. This one serves a directory of files and a web page fetcher:
mkdir -p ~/mcp-server ~/shared
cat > ~/mcp-server/servers.json <<'EOF'
{
"mcpServers": {
"files": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem@2", "/home/exedev/shared"]
},
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
}
}
}
EOF
Put API keys and other secrets in ~/mcp-server.env, not this file;
mcp-proxy passes that environment to every server.
The service runs mcp-proxy on port 8000:
sudo tee /etc/systemd/system/mcp-server.service >/dev/null <<'EOF'
[Unit]
Description=MCP servers (~/mcp-server/servers.json) behind mcp-proxy
After=network-online.target
Wants=network-online.target
[Service]
User=exedev
WorkingDirectory=/home/exedev/mcp-server
EnvironmentFile=/home/exedev/mcp-server.env
Environment=PATH=/home/exedev/.local/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/home/exedev/.local/bin/mcp-proxy --host 0.0.0.0 --port ${PORT} --stateless --pass-environment --named-server-config servers.json
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-server
Each server is at https://my-mcp-server.exe.xyz/servers/<name>/mcp
(here /servers/files/mcp and /servers/fetch/mcp). To add one, edit
servers.json and restart mcp-server.
Check it#
List the tools the way exe.dev's proxy reaches the server: port 8000,
addressed to $(hostname).exe.xyz. Use your server's path (with
mcp-proxy, each /servers/<name>/mcp):
sleep 3
~/bin/mcp-check http://localhost:8000/mcp -H "Host: $(hostname).exe.xyz"
Expect connected: and the tools. For the starter, save a note and find
it:
~/bin/mcp-check http://localhost:8000/mcp --call save_note '{"text": "set up by hand"}'
~/bin/mcp-check http://localhost:8000/mcp --call search_notes '{"query": "hand"}'
A request from another web page gets 403:
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:8000/mcp \
-H "Host: $(hostname).exe.xyz" -H 'Origin: https://example.com' \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
Back up nightly#
This script copies each SQLite database in ~/data with sqlite3 .backup (safe while the server writes) and checks the copy. It adds the
rest of ~/data, the server's code and config (without .venv), and the
settings files, and keeps the 14 newest backups:
cat > ~/bin/mcp-backup <<'EOF'
#!/bin/bash
# Back up ~/data, ~/mcp-server, ~/mcp-gate and their settings to
# ~/backups/mcp-<time>.tar.gz. SQLite databases are copied with .backup,
# safe while the server runs. Keeps the 14 newest.
set -euo pipefail
umask 077
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir -p "$HOME/backups" "$work/data"
rsync -a --exclude='*.db' --exclude='*.db-wal' --exclude='*.db-shm' "$HOME/data/" "$work/data/"
for db in "$HOME"/data/*.db; do
[ -e "$db" ] || continue
copy="$work/data/$(basename "$db")"
sqlite3 "$db" ".backup '$copy'"
[ "$(sqlite3 "$copy" 'PRAGMA integrity_check')" = ok ]
done
for dir in mcp-server mcp-gate; do
[ -d "$HOME/$dir" ] && rsync -a --exclude=.venv --exclude=node_modules --exclude=__pycache__ "$HOME/$dir" "$work/"
done
cp "$HOME"/mcp-*.env "$work/"
out="$HOME/backups/mcp-$(date -u +%Y%m%d-%H%M%S).tar.gz"
tar -czf "$out.part" -C "$work" .
mv "$out.part" "$out"
ls -1t "$HOME"/backups/mcp-*.tar.gz | tail -n +15 | xargs -r rm --
echo "$out"
EOF
chmod +x ~/bin/mcp-backup
Run it nightly, around 03:30 UTC:
sudo tee /etc/systemd/system/mcp-backup.service >/dev/null <<'EOF'
[Unit]
Description=Back up the MCP server's data, code, and settings to ~/backups
[Service]
Type=oneshot
User=exedev
ExecStart=/home/exedev/bin/mcp-backup
EOF
sudo tee /etc/systemd/system/mcp-backup.timer >/dev/null <<'EOF'
[Unit]
Description=Back up the MCP server nightly
[Timer]
OnCalendar=*-*-* 03:30:00 UTC
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-backup.timer
Make one now and check it restores, in a scratch directory:
backup=$(~/bin/mcp-backup)
rm -rf /tmp/restore-check && mkdir /tmp/restore-check
tar -xzf "$backup" -C /tmp/restore-check
ls /tmp/restore-check
for db in /tmp/restore-check/data/*.db; do [ -e "$db" ] && sqlite3 "$db" 'PRAGMA integrity_check'; done
rm -rf /tmp/restore-check
Expect data, mcp-server, and mcp-server.env, and ok for each
database. Copy ~/backups off the VM now and then; otherwise the VM's
disk holds the only copy.
Back up and restore
On the VM, ~/bin/mcp-backup backs up now and prints the archive's path.
Each holds data/ (SQLite as consistent copies), mcp-server/ without
.venv or node_modules, mcp-gate/ if you use it, and the .env
files. To copy them to your computer:
scp 'my-mcp-server.exe.xyz:backups/mcp-*.tar.gz' .
Restore#
On a new VM, first follow Set up by hand through "Check it" for the same kind of server, then copy the archive over from your computer:
scp mcp-20261023-033012.tar.gz my-mcp-server.exe.xyz:backups/
On the VM, stop the services, set the current files aside, and unpack:
backup=~/backups/mcp-20261023-033012.tar.gz
sudo systemctl stop mcp-gate 2>/dev/null; sudo systemctl stop mcp-server
stamp=$(date +%s)
mv ~/data ~/data.before-restore-$stamp
rm -rf /tmp/restore && mkdir /tmp/restore && tar -xzf "$backup" -C /tmp/restore
mv /tmp/restore/data ~/data
rsync -a /tmp/restore/mcp-server/ ~/mcp-server/
[ -d /tmp/restore/mcp-gate ] && mkdir -p ~/mcp-gate && rsync -a /tmp/restore/mcp-gate/ ~/mcp-gate/
for f in /tmp/restore/mcp-*.env; do install -m 600 "$f" ~/; done
(cd ~/mcp-server && [ -f uv.lock ] && uv sync --frozen)
(cd ~/mcp-gate 2>/dev/null && uv sync --frozen)
sudo systemctl start mcp-server
systemctl -q is-enabled mcp-gate 2>/dev/null && sudo systemctl start mcp-gate
rm -rf /tmp/restore
If the backup has mcp-gate/ but this VM never did, follow
Connect claude.ai and ChatGPT from
"Run it under systemd". Then check it, and delete
~/data.before-restore-* once you're happy.
Update the SDK, mcp-proxy, and the rest
On the VM:
- The starter and mcp-gate:
cd ~/mcp-server && uv lock --upgrade && uv sync, likewise in~/mcp-gate, thensudo systemctl restart mcp-server mcp-gateand check.mcp>=2.3,<3holds you on version 2; read the release notes before raising it. - mcp-proxy:
uv tool upgrade mcp-proxystays in the installed range; for a newer one,uv tool install --force 'mcp-proxy>=0.14,<0.15'. - Wrapped servers:
npxanduvxfetch the newest matching version each time the service starts. Pin exact versions inservers.json(server-filesystem@2.0.0,mcp-server-fetch==2026.10.19) to update only when you choose. - Node.js: updates with apt. For a new major line, run NodeSource's
setup script for it (
setup_26.x), thensudo apt-get install -y nodejs. - Ubuntu:
sudo apt-get update && sudo apt-get upgrade -y.
Something not working?
On the VM, the logs and status of the server and mcp-gate:
journalctl -u mcp-server -n 100 --no-pager
systemctl status mcp-server --no-pager
journalctl -u mcp-gate -n 50 --no-pager
401, or an HTML login page. No valid token reached exe.dev. Check the
header name (X-Exedev-Authorization), the Bearer before the token,
and that the token is for this VM and unexpired. To rule out the client,
run check.py on your computer with the same token.
421 Misdirected Request or "Invalid Host header". The server
answers only host names it knows. The starter allows $(hostname).exe.xyz
and localhost; add others (a custom domain) to ALLOWED_HOSTS in
~/mcp-server.env. For a repository's server, find its allowed-hosts
setting (allowed_hosts in the Python SDK's TransportSecuritySettings,
allowedHosts in TypeScript).
403 Invalid Origin header. A browser-based client sent an Origin
the server doesn't trust. Desktop and command-line clients send none.
502 Bad Gateway. Nothing answers on port 8000: the service is
stopped, crashed at start (see the log), or listens on 127.0.0.1 or
another port.
ss -ltnp | grep 800 shows what listens where.
A wrapped server's tools are missing. mcp-proxy logs each server it
starts, with any error. Run the command by hand
(npx -y @modelcontextprotocol/server-filesystem@2 ~/shared) to see it;
curl -s localhost:8000/status lists the servers mcp-proxy knows.
claude.ai can't connect, or sign-in fails. Check that the VM is public
(ssh exe.dev share show my-mcp-server) and that
curl -s https://my-mcp-server.exe.xyz/.well-known/oauth-authorization-server
works from your computer. "Not allowed" on the approval page means your
exe.dev address isn't in ALLOWED_EMAILS. mcp-gate logs each request
and its status.
Long tool calls time out. Clients give up on their own schedule
(Codex at 60 seconds unless you raise tool_timeout_sec). For
minutes-long work, have one tool start a job and return an id, and
another report on it.
Frequently asked questions#
Does my MCP server need OAuth?
Only for clients that can't send their own header: claude.ai, ChatGPT, and their apps. Claude Code, Codex, Cursor, VS Code, and the Claude and OpenAI APIs send a VM token that exe.dev checks, so your server stays private with no auth code. For the rest, add mcp-gate, which signs people in with exe.dev.
Can claude.ai or ChatGPT reach a private MCP server?
No. They connect from their own servers, with OAuth only, so the server must be public. With mcp-gate, every request still needs a sign-in from an exe.dev account you listed.
Streamable HTTP or SSE?
Streamable HTTP, at one URL such as /mcp. The older HTTP+SSE transport is deprecated but still common. The starter speaks only Streamable HTTP; mcp-proxy serves both, SSE at /servers/<name>/sse.
Can one VM host several MCP servers?
Yes. mcp-proxy serves any number of stdio servers at /servers/<name>/mcp, and other servers can use other ports: https://my-mcp-server.exe.xyz:3000/mcp reaches port 3000, always private, with the same VM token.
Why a VM rather than a serverless platform?
The current MCP spec no longer needs long-lived sessions, so a server wrapping an API runs fine on serverless. A VM suits tools that need a computer: a disk for files and databases, git checkouts, a browser, minutes-long jobs, a shell. It never sleeps, has no cold starts, and its agent can change the server for you.
Do I have to write it in Python?
No. The starter uses the official Python SDK for brevity, but the VM runs anything: TypeScript with @modelcontextprotocol/sdk, Go, Rust, a Docker image. Hosting, tokens, and mcp-gate work the same.
Who can use my tools?
While private: you, people you share the VM with, and anything holding one of your VM tokens. With mcp-gate, also apps approved by accounts in ALLOWED_EMAILS. All can call every tool, with the server's access to the VM, so keep each token's --exp short.
Updated Oct 23, 2026 · Markdown · manifest.json · Something wrong? Tell us