Skip to content

Host a remote MCP server

Tools for all your agents, on a computer of their own. Private until you say.

Deploy on exe.dev

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/stdin

An 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>/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.
What's running, and where
Private https://my-mcp-server.exe.xyz/mcp the MCP endpoint, for you and agents with a VM token
Private http://notes.int.exe.xyz/mcp the same, for agents on your other VMs (VM-to-VM integration)
Public https://my-mcp-server.exe.xyz/mcp with mcp-gate only: OAuth sign-in for claude.ai, ChatGPT, and their apps
my-mcp-server.exe.xyz exe.dev VM
Your MCP server the notes starter (official Python SDK), your repository, or stdio servers behind mcp-proxy, as mcp-server.service :8000~/mcp-server
SQLite the starter's notes, full-text searchable ~/data
mcp-gate (optional) OAuth via exe.dev login, for claude.ai and ChatGPT; takes port 8000 and moves the server to 8001 :8000~/data/mcp-gate.db
Nightly backup a systemd timer; keeps two weeks ~/backups

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: 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
Prefer a terminal?
curl -fsSL \
  https://exe-staging.dev/deploy/mcp-server/prompt.txt \
  | ssh exe.dev new --prompt=/dev/stdin

When 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 you
What 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, then ssh exe.dev ssh-key remove with 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.service instead of the starter.
  • A stdio server, the kind you'd run with npx or uvx from 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, formerly FastMCP): mcp.run("streamable-http", host="0.0.0.0", port=int(os.environ["PORT"])).
  • TypeScript SDK: a StreamableHTTPServerTransport in an HTTP server on process.env.PORT.
  • Otherwise: look for a --transport http (or streamable-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, then sudo systemctl restart mcp-server mcp-gate and check. mcp>=2.3,<3 holds you on version 2; read the release notes before raising it.
  • mcp-proxy: uv tool upgrade mcp-proxy stays in the installed range; for a newer one, uv tool install --force 'mcp-proxy>=0.14,<0.15'.
  • Wrapped servers: npx and uvx fetch the newest matching version each time the service starts. Pin exact versions in servers.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), then sudo 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