---
title: "Host a remote MCP server"
description: "Host a remote MCP server on an exe.dev VM: Streamable HTTP over HTTPS, private behind exe.dev login, OAuth for claude.ai and ChatGPT, SQLite on disk."
url: https://exe-staging.dev/deploy/mcp-server
category: "AI agents"
updated: 2026-10-23
---

# Host a remote MCP server

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

- **Software:** Remote MCP server (https://modelcontextprotocol.io)
- **Source:** https://github.com/modelcontextprotocol/python-sdk
- **License:** MIT
- **Time:** 10 minutes
- **Example VM name:** `my-mcp-server` (yours will differ; on the VM, `hostname` prints it)
- **Machine-readable:** https://exe-staging.dev/deploy/mcp-server/manifest.json
- **File:** https://exe-staging.dev/deploy/mcp-server/check.py
- **File:** https://exe-staging.dev/deploy/mcp-server/mcp-gate.py
- **File:** https://exe-staging.dev/deploy/mcp-server/server.py

## 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.

Running on the VM:

- **Your MCP server**: the notes starter (official Python SDK), your repository, or stdio servers behind mcp-proxy, as `mcp-server.service`, port 8000, data in `~/mcp-server`
- **SQLite**: the starter's notes, full-text searchable, data in `~/data`
- **mcp-gate (optional)**: OAuth via exe.dev login, for claude.ai and ChatGPT; takes port 8000 and moves the server to 8001, port 8000, data in `~/data/mcp-gate.db`
- **Nightly backup**: a systemd timer; keeps two weeks, data in `~/backups`

How it is reached:

- https://my-mcp-server.exe.xyz/mcp (private): the MCP endpoint, for you and agents with a VM token
- http://notes.int.exe.xyz/mcp (private): the same, for agents on your other VMs (VM-to-VM integration)
- https://my-mcp-server.exe.xyz/mcp (public): with mcp-gate only: OAuth sign-in for claude.ai, ChatGPT, and their apps

## Deploy with Shelley

Open https://exe-staging.dev/new?prompt=SERVER%3D%3Cfill-this-in%3E%0A%0ASet%20up%20Remote%20MCP%20server%20on%20this%20VM%20by%20following%20the%20exe.dev%20recipe%20at%20https%3A%2F%2Fexe-staging.dev%2Fdeploy%2Fmcp-server.md%20from%20top%20to%20bottom.%0A%0AThe%20values%20at%20the%20top%20of%20this%20message%20are%20mine%3A%0A-%20SERVER%3A%20%60new%60%20for%20the%20notes%20starter%3B%20a%20GitHub%20repository%20%28owner%2Fname%20or%20URL%29%20with%20an%20MCP%20server%3B%20or%20a%20stdio%20server%27s%20command%20%28%60npx%20-y%20%40modelcontextprotocol%2Fserver-filesystem%402%20~%2Fshared%60%29%20or%20Claude%20Desktop%20config%0AAsk%20me%20for%20any%20that%20still%20say%20%3Cfill-this-in%3E%20before%20you%20need%20them.%0A%0AThe%20recipe%27s%20example%20VM%20name%20is%20my-mcp-server.%20This%20VM%27s%20name%20is%20the%20output%20of%20%60hostname%60%3A%20use%20it%20wherever%20the%20recipe%20writes%20my-mcp-server.%20Skip%20the%20step%20that%20creates%20the%20VM%3B%20you%20are%20on%20it.%0A%0AYou%20are%20done%20when%20all%20of%20these%20are%20true.%20Check%20each%20one%20yourself%3A%0A-%20An%20MCP%20server%20on%20Streamable%20HTTP%20at%20%60https%3A%2F%2Fmy-mcp-server.exe.xyz%2Fmcp%60%20%28%60%2Fservers%2F%3Cname%3E%2Fmcp%60%20for%20stdio%20servers%29%20as%20%60mcp-server.service%60%2C%20restarted%20on%20failure%20and%20reboot%3A%20the%20starter%2C%20your%20repository%2C%20or%20stdio%20servers%20behind%20mcp-proxy.%0A-%20Private%20behind%20exe.dev%3A%20clients%20send%20a%20VM%20token%20in%20one%20header%2C%20and%20your%20server%20reads%20the%20caller%20from%20%60X-ExeDev-Email%60%20with%20no%20auth%20code.%0A-%20%60~%2Fbin%2Fmcp-check%60%2C%20a%20small%20client%20that%20lists%20the%20tools%20and%20calls%20one%2C%20run%20against%20the%20server%20as%20exe.dev%27s%20proxy%20reaches%20it.%0A-%20Code%2C%20%60~%2Fdata%60%2C%20and%20settings%20backed%20up%20nightly%20to%20%60~%2Fbackups%60%2C%20with%20a%20restore%20checked%20in%20a%20scratch%20directory.%0A%0ARead%20the%20whole%20recipe%2C%20closed%20details%20included%2C%20and%20follow%20%22Set%20up%20by%0Ahand%22%20in%20order%2C%20skipping%20VM%20creation%3B%20the%20walkthrough%20above%20it%20is%20for%0Athe%20owner.%20Ask%20for%20SERVER%20first%20if%20it%20says%20%3Cfill-this-in%3E.%20For%20%60new%60%2C%0Abuild%20the%20starter%20exactly%20as%20written%3B%20for%20a%20repository%2C%20follow%20%22Or%3A%20a%0Aserver%20from%20a%20repository%22%3B%20for%20a%20stdio%20server%20or%20Claude%20Desktop%20config%2C%0A%22Or%3A%20stdio%20servers%22.%20Ask%20the%20owner%20for%20any%20secret%20a%20server%20needs%20and%20put%0Ait%20in%20~%2Fmcp-server.env%2C%20never%20in%20servers.json%20or%20the%20unit.%20If%20a%20private%0Arepository%20won%27t%20clone%2C%20give%20the%20owner%20the%20%60integrations%20add%20github%60%0Acommand%20with%20this%20VM%27s%20name%20and%20wait.%0ASet%20up%20mcp-gate%20only%20if%20the%20owner%20asks%20for%20claude.ai%2C%20ChatGPT%2C%20or%20their%0Aapps%2C%20following%20%22Connect%20claude.ai%20and%20ChatGPT%22%20on%20the%20VM%3B%20don%27t%20make%0Athe%20VM%20public%2C%20but%20give%20the%20owner%20the%20%60share%20set-public%60%20command.%0ADelete%20notes%20you%20save%20while%20checking.%20Don%27t%20reboot%20or%20restore%20over%20live%0Adata%3A%20test%20the%20backup%20in%20a%20scratch%20directory%20as%20written.%20The%20VM%20can%27t%0Aopen%20its%20own%20https%3A%2F%2F%3Cvm%3E.exe.xyz%3B%20%22Check%20it%22%20goes%20through%20localhost.%0AEnd%20with%20each%20MCP%20URL%2C%20a%20short%20name%20and%20the%20tools%20for%20each%2C%20and%0Aanything%20you%20adapted%20or%20the%20owner%20should%20change.%0A%0AFinish%20with%20the%20URLs%20I%20should%20open%20and%20these%20commands%2C%20which%20only%20I%20can%20run%2C%20from%20my%20own%20computer%20%28with%20this%20VM%27s%20name%20in%20them%29%3A%0A%20%20ssh%20exe.dev%20ssh-key%20generate-api-key%20--vm%3Dmy-mcp-server%20--label%3Dmcp-clients%20--exp%3D90d%0A%20%20claude%20mcp%20add%20--transport%20http%20--scope%20user%20notes%20https%3A%2F%2Fmy-mcp-server.exe.xyz%2Fmcp%20--header%20%22X-Exedev-Authorization%3A%20Bearer%20exe1.paste-yours-here%22%20%20%20%23%20or%20the%20name%20and%20URL%20Shelley%20gave%20you%0A to create a VM whose agent, Shelley, follows this recipe. From a terminal:

```sh
curl -fsSL https://exe-staging.dev/deploy/mcp-server/prompt.txt | ssh exe.dev new --prompt=/dev/stdin
```

Then, on your own computer:

```sh
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
```

Or follow the steps below by hand.

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.

## Connect your agents

Agents can't sign in with a browser, so make them a VM token on your
computer (`--exp` sets its expiry):

```bash
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](/docs/https-tokens-for-vms)).

[check.py](https://exe-staging.dev/deploy/mcp-server/check.py) connects like an
agent, lists the tools, and can call one:

```bash
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"
```

```text
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`):

```bash
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`:

```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):

```json
{
  "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):

```json
{
  "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](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector)
takes the token as `authorization_token`; OpenAI's Responses API takes it
in the MCP tool's `headers`:

```python
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](https://exe-staging.dev/deploy/mcp-server/server.py):

```python
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:

```bash
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](/docs/integrations-vm-to-vm). On your
computer, with your agent's VM for `my-agent-vm`:

```bash
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:

```bash
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](https://exe-staging.dev/deploy/mcp-server/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):

```bash
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:

```bash
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:

```bash
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):

```bash
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](https://developers.openai.com/api/docs/guides/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](#create-the-vm):

- **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](https://github.com/sparfenyuk/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.

<details id="create-the-vm" data-manual-setup class="dp-details">
<summary>Set up by hand</summary>

### Create the VM

On your computer (pick another name if `my-mcp-server` is taken):

```bash
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](#connect-your-agents) as `~/bin/mcp-check` (uv
installs its one dependency on first run):

```bash
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](https://exe-staging.dev/deploy/mcp-server/server.py) is a 150-line
notes server on the official
[MCP Python SDK](https://github.com/modelcontextprotocol/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.

```bash
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:

```bash
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](#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:

```bash
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](/docs/integrations-github),
then clone through it, with no token on the VM:

```bash
ssh exe.dev integrations add github --name NAME --repository OWNER/NAME --attach vm:my-mcp-server
```

```bash
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](#or-stdio-servers).

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](https://github.com/sparfenyuk/mcp-proxy) starts each one you
list and serves it over HTTP. Install it, plus Node.js 24 from NodeSource
for `npx` servers:

```bash
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:

```bash
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:

```bash
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`):

```bash
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:

```bash
~/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`:

```bash
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:

```bash
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:

```bash
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:

```bash
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.

</details>

<details id="back-up-and-restore" class="dp-details">
<summary>Back up and restore</summary>

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:

```bash
scp 'my-mcp-server.exe.xyz:backups/mcp-*.tar.gz' .
```

### Restore

On a new VM, first follow [Set up by hand](#create-the-vm) through
"Check it" for the same kind of server, then copy the archive over from
your computer:

```bash
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:

```bash
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](#connect-claude-ai-and-chatgpt) from
"Run it under systemd". Then [check it](#check-it), and delete
`~/data.before-restore-*` once you're happy.

</details>

<details id="update" class="dp-details">
<summary>Update the SDK, mcp-proxy, and the rest</summary>

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](https://github.com/modelcontextprotocol/python-sdk/releases)
  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`.

</details>

<details id="troubleshooting" class="dp-details">
<summary>Something not working?</summary>

On the VM, the logs and status of the server and mcp-gate:

```bash
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.

</details>

## 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](#connect-claude-ai-and-chatgpt), 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](/docs/sharing), 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.


## Related

- [Run Claude Code on a remote server](https://exe-staging.dev/deploy/claude-code.md): A computer for Claude. A breather for your laptop.
- [Run Codex on a remote server](https://exe-staging.dev/deploy/codex.md): A computer for Codex. An early night for your laptop.
- [Deploy a FastAPI app](https://exe-staging.dev/deploy/fastapi.md): Your API on a VM of its own. Push to deploy; the data stays put.
- [Running Agents](https://exe-staging.dev/deploy/agents.md): Use exe.dev VMs as a sandbox
- [Host OpenClaw](https://exe-staging.dev/deploy/openclaw.md): An AI sidekick with its own computer. Your laptop can take a nap.
- [Run OpenCode on a remote server](https://exe-staging.dev/deploy/opencode.md): Every model, one VM, and an agent that works while your laptop sleeps.
