Host a Remote MCP Server on Your Own Domain
Short answer: A remote MCP server is an MCP server that runs on a machine with a public HTTPS address instead of on your laptop, so Claude Code and other AI clients can use its tools from anywhere. You need three things: the server itself speaking MCP's Streamable HTTP transport, an HTTPS endpoint on a domain, and a way to keep strangers out. This guide builds a small Python MCP server with the official SDK, protects it with a bearer token, and runs it on a $4/month NameOcean App Server at https://mcp.example.com/mcp. Every file below was tested with MCP SDK 2.3, behind the same nginx setup the App Server uses.
Local vs. remote MCP servers
- Local (stdio): the AI client starts the server as a process on your computer. Simple, but it only works on that machine and dies with it.
- Remote (Streamable HTTP): the server runs elsewhere and the client sends JSON-RPC requests to one URL, usually ending in
/mcp. Your whole team, a CI job and every device can share it, and it can reach data your laptop can't, such as a database on the same server.
Host it yourself when the tools touch your own systems (an internal API, a database, files), when you don't want a third party in the middle, or when a hosted MCP platform charges per seat for something a small server can do.
Got an idea? Find the perfect domain for it
Our AI domain name generator suggests available names across hundreds of extensions. Free SSL, DNS and Whois privacy included.
1. Write the server
The project has three files. server.py defines three tools (save a note, list notes, tell the time), stores the notes in SQLite and refuses any request without the right token:
import hmac
import os
import sqlite3
from datetime import datetime, timezone
from mcp.server import MCPServer
from mcp.server.transport_security import TransportSecuritySettings
TOKEN = os.environ["MCP_TOKEN"]
HOST = os.environ["MCP_HOST"] # the public name, e.g. mcp.example.com
DB = os.environ.get("NOTES_DB", "/root/data/notes.db")
mcp = MCPServer("notes")
def db():
conn = sqlite3.connect(DB)
conn.execute("create table if not exists notes (id integer primary key, text text not null)")
return conn
@mcp.tool()
def add_note(text: str) -> str:
"""Save a short note."""
with db() as conn:
note_id = conn.execute("insert into notes (text) values (?)", (text,)).lastrowid
return f"Saved note {note_id}."
@mcp.tool()
def list_notes() -> list[str]:
"""Return every saved note, oldest first."""
with db() as conn:
return [row[0] for row in conn.execute("select text from notes order by id")]
@mcp.tool()
def utc_now() -> str:
"""The current time in UTC."""
return datetime.now(timezone.utc).isoformat(timespec="seconds")
mcp_app = mcp.streamable_http_app(
stateless_http=True, # no session state: any process can answer any request
json_response=True, # plain JSON answers, no long-lived event streams
transport_security=TransportSecuritySettings(allowed_hosts=[HOST], allowed_origins=[f"https://{HOST}"]),
)
async def app(scope, receive, send):
"""Refuse every HTTP request without the right bearer token."""
if scope["type"] == "http":
auth = dict(scope["headers"]).get(b"authorization", b"").decode()
if not hmac.compare_digest(auth, f"Bearer {TOKEN}"):
await send({"type": "http.response.start", "status": 401,
"headers": [(b"content-type", b"application/json"), (b"www-authenticate", b"Bearer")]})
await send({"type": "http.response.body", "body": b'{"error": "unauthorized"}'})
return
await mcp_app(scope, receive, send)
Three settings matter for hosting:
stateless_http=True: every request stands alone, so you can run several worker processes and restart them at any time without breaking a client's session.json_response=True: answers come back as plain JSON instead of an event stream. Reverse proxies buffer and time out long streams; short JSON answers pass through any of them.transport_security: the SDK only accepts requests for the host names you list, which blocks DNS rebinding attacks. A request for any other name gets421 Misdirected Request.
requirements.txt:
mcp>=2.3,<3
uvicorn
.nameocean/deploy.sh runs on the server after every push:
#!/bin/bash
# runs on the server after every git push
set -e
mkdir -p /root/data
/root/.pyenv/versions/app/bin/pip install -q -r requirements.txt
Try it locally first: MCP_TOKEN=dev MCP_HOST=127.0.0.1:8000 NOTES_DB=notes.db uvicorn server:app --port 8000.
2. Create the server and a domain
Create an App Server in the dashboard or with the nameocean CLI. The smallest plan (2 vCPU, 1 GB RAM, $4/month) is plenty for an MCP server. Every server gets a default HTTPS name like 3f9a1c2e.nameocean.org; to use your own, bind a domain and the certificate is issued for you:
nameocean bind mcp.example.com 3f9a1c2e
Then set the token and the host name as the server's env variables. They are kept on the server, and every app on it gets them:
nameocean servers env set 3f9a1c2e \
MCP_TOKEN=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))") \
MCP_HOST=mcp.example.com
nameocean servers env ls 3f9a1c2e # copy MCP_TOKEN for your AI client
3. Run it behind nginx
On an App Server, HTTPS ends at NameOcean's load balancer, which forwards to nginx on the server, which proxies to your app on 127.0.0.1:8000. Connect once with SSH and do two things.
Add the app to /root/conf/supervisor.conf, so it starts at boot and restarts if it crashes:
[program:app]
command=/root/.pyenv/versions/app/bin/uvicorn server:app --host 127.0.0.1 --port 8000 --workers 2
directory=/root/app
environment=PATH="/root/.pyenv/versions/app/bin:/usr/local/bin:/usr/bin:/bin"
user=root
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=/var/log/supervisor/app.log
In /root/conf/nginx.conf, remove the static location / block and uncomment the proxy block below it, then apply both:
supervisorctl reread && supervisorctl update
nginx -t && systemctl reload nginx
4. Deploy
From your project:
nameocean deploy 3f9a1c2e
It pushes the code to the server, where .nameocean/deploy.sh installs the requirements, then restarts the apps and reloads nginx. Every later change is the same command. Check that strangers are kept out:
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://mcp.example.com/mcp
401
5. Connect your AI client
In Claude Code:
claude mcp add --transport http notes https://mcp.example.com/mcp \
--header "Authorization: Bearer $MCP_TOKEN"
Add --scope user to have it in every project, or --scope project to share it with your team through .mcp.json (keep the token itself out of the repository). Then ask: "save a note: renew the TLS certificate on Friday".
From Python, the SDK's client works the same way:
import asyncio
import os
import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
async def main():
headers = {"Authorization": f"Bearer {os.environ['MCP_TOKEN']}"}
async with httpx2.AsyncClient(headers=headers, timeout=httpx2.Timeout(30.0)) as http:
async with Client(streamable_http_client("https://mcp.example.com/mcp", http_client=http)) as client:
print((await client.call_tool("add_note", {"text": "renew the TLS certificate"})).structured_content)
print((await client.call_tool("list_notes", {})).structured_content)
asyncio.run(main())
Things to know
- Keep tool calls under a minute. The load balancer waits up to 60 seconds for an answer. For longer work, start a background job in the tool and add a second tool that returns its result.
- One token is one key to everything. Use a long random token (
python3 -c "import secrets; print(secrets.token_urlsafe(32))"), give each person or client their own if you need to tell them apart, and change it withnameocean servers env setwhen someone leaves. - OAuth: clients that can't send a custom header need OAuth instead of a fixed token. The SDK supports that with its
TokenVerifierandAuthSettings; the token check above is the simple version for clients you configure yourself. - Logs:
nameocean servers logs 3f9a1c2e --app app, or/var/log/supervisor/app.logon the server. - Backups: the notes live in
/root/data/notes.db, which the server's daily backups include.
FAQ
What is the difference between SSE and Streamable HTTP?
The older HTTP+SSE transport used two endpoints and a long-lived event stream. Streamable HTTP, the current transport, uses one endpoint, can answer with plain JSON, and works through ordinary reverse proxies.
Can I run an MCP server written in TypeScript or Go?
Yes. The App Server has Node.js and Go too. Run the server on 127.0.0.1:8000 under supervisor and the rest of this guide is the same.
Does NameOcean have its own MCP server?
Yes: https://nameocean.net/mcp manages your domains, DNS, SSL and servers from Claude Code or Cursor. See MCP for developers.
Ready to host yours? See the App Server plans and the App Server API, or find a domain below for your MCP server's address.
Got an idea? Find the perfect domain for it
Our AI domain name generator suggests available names across hundreds of extensions. Free SSL, DNS and Whois privacy included.
Read in other languages: