Most MCP servers start on a laptop. The client launches them as a subprocess, they speak JSON-RPC over stdin and stdout, and they exist only for as long as that client is running. Hosting one is a different job.
The short answer: hosting an MCP server takes three things. The Streamable HTTP transport instead of stdio, a process that stays running whether or not a client is attached, and a public HTTPS URL with authentication in front of it. Get those right and any authorized client can reach the same server from anywhere.
This guide covers what actually changes when a server goes remote, how the hosting options compare, how to build and deploy one, and, in the section that matters most, how to keep a public MCP endpoint from becoming an open door into your systems.
What does hosting an MCP server require?
HTTP transport, not stdio
The MCP specification defines exactly two standard transports. In stdio, the client launches the server as a subprocess and exchanges newline delimited JSON-RPC over its standard streams. In Streamable HTTP, each message is an HTTP POST to a single MCP endpoint, and the reply comes back either as one JSON object or as an SSE stream scoped to that request.
stdio cannot be hosted. There is no network in the picture at all: the transport is the pipe between a parent process and its child. If you want a server that lives somewhere other than the machine running the client, Streamable HTTP is the only standard option.
Two details are worth knowing before you write code. The server must expose a single endpoint path that accepts POST, conventionally /mcp. And the 2026-07-28 revision made the protocol core stateless: it removed the initialization handshake, the Mcp-Session-Id header and the standalone GET stream, so every request carries its own protocol version and client capabilities in _meta, and any instance can answer any request. That revision also made the MCP-Protocol-Version, Mcp-Method and Mcp-Name request headers required, so gateways can route and rate limit without parsing the body. The older HTTP+SSE transport from 2024-11-05 is deprecated and new servers should not adopt it.
A process that stays running
A stdio server is created and destroyed on demand. A hosted server is the opposite: it has to be listening before the first request arrives, because nothing is going to start it. That means a supervised process, a restart policy when it crashes, and a host that does not put it to sleep between calls.
This is the requirement people underestimate. An MCP tool call is interactive. Someone is waiting on the other end of it, usually inside a conversation. A server that has to boot before it can answer turns every first call of the day into a visible pause.
A public URL, TLS, and something guarding it
Hosted MCP clients connect over the public internet, and they expect HTTPS. Anthropic's documentation is direct about this: Claude connects to your remote MCP server from Anthropic's cloud infrastructure, not from your device, so a server behind a VPN or a corporate firewall will not connect at all.
Public reachability and authentication are the same decision made twice. Once the endpoint resolves from the internet, the only thing standing between an anonymous caller and your tools is the check you wrote.
Local stdio vs remote HTTP: what actually changes
| Dimension | Local stdio server | Remote HTTP server |
|---|---|---|
| Access | Only the client on that machine, which launches it as a subprocess | Any client that can resolve the URL and present valid credentials |
| Authentication | None needed. The spec says stdio servers should take credentials from the environment | Required in practice. The spec models the server as an OAuth 2.1 resource server |
| Operations | Nothing to run. It starts with the client and dies with it | Yours. A host, TLS, restarts, logs, and a deployment pipeline |
| Cost | Free. It borrows your machine | A small always on container, plus a domain if you want a clean URL |
| Team use | One person per copy. Every teammate installs and configures their own | One deployment. A tool change ships once and reaches everyone |
| Best for | Personal tools, local files, machine specific credentials | Shared team tools, unattended agents, anything a hosted client must reach |
The row that surprises people is authentication. On stdio there is no authentication problem, because there is no listener. Moving to HTTP does not add a feature, it adds a threat model.
Where should you host an MCP server?
| Option | Operational load | TLS | Always on | Best for |
|---|---|---|---|---|
| Your own VPS | High. OS patching, process supervision, certificate renewal, firewall rules, backups | You configure it, usually a reverse proxy plus an ACME client | Yes, as long as you keep the process supervised | Teams already running servers who want the lowest fixed monthly cost |
| Container platform (Out Plane) | Low. Push a repo or an image; builds, restarts, routing and certificates are handled | Automatic on the platform domain and on custom domains | Yes. At least one instance always runs, so there is no cold start | Interactive servers where the first tool call should not wait on a boot |
| Serverless function | Low, but the constraints reach into your code: execution time limits, no in-process state, a lifecycle per request | Provided by the platform | No. The process is created to serve a request and torn down | Cheap, bursty, read-only tools that tolerate a cold start |
| Local (stdio) | None | Not applicable. Nothing is listening | No. It lives as long as the client | Personal tools on a single machine |
Each of these is the right answer somewhere.
A VPS is genuinely cheap. A few euros a month buys a machine that will happily run a small Python process forever. What the price excludes is the work: you install the runtime, write a systemd unit, put nginx or Caddy in front, wire up certificate renewal, and own every security update. For a team with an ops habit this is fine. For a team without one it is a second job that only announces itself when a certificate expires.
Serverless is the cheapest way to run something rarely. The stateless direction of the 2026-07-28 spec helps here, because a per-request lifecycle no longer fights a session-based protocol. The honest tradeoff is the cold start. A function that has been idle takes a noticeable moment to answer, and that moment lands inside a conversation where a human is waiting. Long running tools also collide with execution time limits.
A container platform sits between the two. You give it a repository or an image and it gives you a running container behind TLS. On Out Plane the practical advantages for this workload are narrow but real: at least one instance is always running, so there is no cold start on the first tool call; certificates are issued automatically for the platform domain and for custom domains; logs, request traces and metrics are there without adding an agent; and a browser shell lets you open a terminal inside a running instance when a tool misbehaves in a way the logs do not explain. Compute is metered by the minute, and instance count is something you set yourself up to your plan ceiling rather than something that scales on its own.
Building the server with HTTP transport
The example below uses FastMCP 3.x, which is the shortest path to a working Python server. The official Python SDK works equally well and its 2.x line implements the 2026-07-28 specification. Whichever you pick, check which protocol revision your version speaks, because the transport changed meaningfully in that revision.
from fastmcp import FastMCP
mcp = FastMCP("My Production MCP Server")
@mcp.tool()
def search_docs(query: str) -> str:
"""Search internal documentation and return matching passages."""
return perform_search(query)
@mcp.tool()
def get_status(service: str) -> dict:
"""Report the current health of a named service."""
return {"service": service, "status": "healthy"}
@mcp.resource("config://app")
def get_config() -> str:
"""Non-secret application configuration."""
return "region=eu-central, log_level=info"
if __name__ == "__main__":
mcp.run(transport="http", host="0.0.0.0", port=8080)FastMCP accepts stdio (the default), http, and a legacy sse value, and serves the MCP endpoint at /mcp. So a container listening on 8080 exposes https://your-app.outplane.app/mcp once it is deployed.
The host="0.0.0.0" line deserves a note, because the specification says the opposite. Its Streamable HTTP section states that when running locally, servers should bind only to 127.0.0.1 rather than all interfaces. That advice is about a laptop, where every process on the machine and every page in your browser is a potential caller. Inside a container the isolation boundary is the container itself: binding to 127.0.0.1 there means nothing outside the container can reach the process, and the platform will never route traffic to it. Bind to 0.0.0.0 in the container, and bind to 127.0.0.1 when you run the same file on your own machine.
For production traffic, running the app under an ASGI server is worth the extra line. FastMCP exposes mcp.http_app() for exactly that, so you can hand the application object to uvicorn with the worker count and timeouts you want.
Packaging it
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["python", "server.py"]fastmcp>=3.0.0Three files, pushed to GitHub: server.py, requirements.txt, Dockerfile.
Deploying it
From the console, create an application, pick the repository, and choose Dockerfile as the build method. Set the port to 8080 so it matches mcp.run(). There is no region to choose: every application runs in Nuremberg, Germany, which is also what keeps the data in the EU. There is no autoscaling setting either; you choose an instance size and a replica count, and both stay where you put them.
Add any credentials your tools need as environment variables before the first deploy, so the process finds them at startup. Then deploy. The build runs, the container starts, and the app comes up at https://your-app.outplane.app, with /mcp as the MCP endpoint. Every later push to the selected branch redeploys it.
The same thing from a terminal, using the outplane CLI:
curl -fsSL https://outplane.com/install.sh | sh
outplane login
outplane app create mcpserver \
--repo yourname/mcp-server --branch main \
--port 8080:http:public \
--env MCP_API_KEY="$(openssl rand -hex 32)"
outplane logs mcpserver --followCreating also deploys, so there is no separate deploy step on the first run; later ones are outplane deploy create mcpserver --wait. The name takes letters and numbers only, 5 to 45 characters, and it cannot be changed afterwards, because it appears in every address the application answers on.
Commands that change something take --dry-run and print the request instead of sending it, apart from a handful whose whole effect is local, and output turns into JSON when piped, which makes the CLI usable from a script or from an agent.
If you would rather describe the change than perform it, Otto, the AI agent built into the console, can create the application, set variables and read back the deployment for you. It works through the same public API, so there is nothing it can do that you cannot see and undo.
How do you secure a remote MCP server?
This is the part most MCP tutorials skip, and it is the part that decides whether hosting your server was a good idea.
Assume the endpoint will be found
An MCP endpoint is an ordinary HTTP endpoint on the public internet. It gets crawled, scanned and fuzzed like everything else. An unauthenticated MCP server is a remote execution surface wearing a JSON-RPC costume: whatever your tools can do, an anonymous caller can do. If a tool queries your database, the internet queries your database.
Authenticate every request
The specification makes authorization optional overall but says HTTP based implementations should conform to it, and the authorization spec is specific about what conforming means. The MCP server acts as an OAuth 2.1 resource server. It must implement OAuth 2.0 Protected Resource Metadata (RFC 9728) so clients can discover the authorization server, answer an unauthenticated call with 401 and a WWW-Authenticate: Bearer resource_metadata="..." challenge, and, critically, validate that the access token was issued specifically for it as the audience. A server that accepts any well formed token becomes a confused deputy, happily acting on a token minted for somebody else's service.
Full OAuth is a real project. For an internal server used by your own team, a long random shared secret checked on every request is a defensible first step, as long as it is stored as an environment variable, compared in constant time, and rotatable without a code change:
import hmac
import os
from fastmcp import FastMCP
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
API_KEY = os.environ["MCP_API_KEY"]
mcp = FastMCP("Secured MCP Server")
class BearerTokenMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
header = request.headers.get("authorization", "")
scheme, _, token = header.partition(" ")
if scheme.lower() != "bearer" or not hmac.compare_digest(token, API_KEY):
return JSONResponse({"error": "unauthorized"}, status_code=401)
return await call_next(request)Two rules apply whichever scheme you choose. Credentials go in headers, never in the URL: the spec forbids access tokens in the query string, and Anthropic's connector guidance points out that a URL with an embedded token leaks through server logs, proxy logs, browser history, analytics and screenshots. And the comparison must be constant time, because == on a secret leaks its prefix to anyone patient enough to measure.
Validate the Origin header
The spec is unusually blunt here: servers must validate the Origin header on all incoming connections to prevent DNS rebinding attacks, and must answer 403 Forbidden when a present Origin is invalid. Without it, a page in someone's browser can be steered into talking to your server with their network position. Most frameworks give you this in a few lines. Add them.
Design tools that cannot do much damage
Authentication decides who calls your server. Tool design decides how much a successful call is worth. Two habits carry most of the weight.
Make tools read-only unless a write is the point. A list_orders tool and a refund_order tool deserve very different scrutiny, and it is worth asking whether the second belongs on a shared server at all.
Never build queries out of model supplied strings. Allowlist every identifier that reaches SQL, not just the ones you thought of first:
from fastmcp.exceptions import ToolError
ALLOWED_TABLES = {"products", "orders", "customers"}
ALLOWED_COLUMNS = {"name", "email", "sku", "status"}
@mcp.tool()
def search_records(table: str, column: str, value: str) -> list:
"""Search an allowlisted table by an allowlisted column."""
if table not in ALLOWED_TABLES or column not in ALLOWED_COLUMNS:
raise ToolError(
f"Not searchable. Tables: {sorted(ALLOWED_TABLES)}. Columns: {sorted(ALLOWED_COLUMNS)}."
)
with get_connection() as conn, conn.cursor() as cur:
cur.execute(
f"SELECT * FROM {table} WHERE {column} ILIKE %s LIMIT 50",
(f"%{value}%",),
)
return cur.fetchall()The parameter placeholder protects value. Only the allowlists protect table and column, because an identifier cannot be passed as a bound parameter. Allowlisting the table and interpolating the column is a common and dangerous half measure.
Keep secrets out of the repository
API keys, database URLs and tokens belong in environment variables, injected at deploy time. On Out Plane you set them per application or in an environment group shared across several, and changing one triggers a redeploy so the new value is actually in the process. Our guide to environment variables and secrets covers rotation and the mistakes that survive code review.
Restrict who can reach it
If only your own infrastructure calls the server, put an IP access profile in front of it and let the network reject everything else before your code runs.
One caveat that catches people out: since Claude connects from Anthropic's cloud rather than from your laptop, an allowlist built from office addresses will block it. Anthropic publishes the IP ranges to allow for exactly this case. Allowlisting is a good control for machine to machine callers and a poor one for hosted assistants, unless you use their published ranges.
Connecting a client
For Claude, a hosted remote server is added as a custom connector rather than edited into a JSON file. On Pro and Max plans the path is Customize → Connectors → + → Add custom connector, where you enter a name and the server URL, with optional OAuth client credentials under advanced settings. On Team and Enterprise plans an owner adds it once under organization settings and members connect individually. Free accounts are limited to one custom connector.
From a terminal, Claude Code takes the same server as a flag:
claude mcp add --transport http my-server https://your-app.outplane.app/mcp \
--header "Authorization: Bearer $MCP_API_KEY"In JSON configuration, streamable-http is accepted as an alias for http, so a config copied from server documentation that uses the specification's name works without editing. Any other MCP client library connects to the same URL; nothing about the endpoint is client specific.
Adding a custom domain
The generated .outplane.app URL works immediately, but a stable mcp.yourdomain.com is easier to put in documentation and survives an application rename. Map the domain in the console under Domains, or run outplane domain add mcp.yourdomain.com --app mcpserver --port 8080, then add the CNAME record it gives you at your registrar pointing to domains-management.outplane.app. The certificate is issued automatically once DNS resolves.
When something does not work
The app builds but never becomes ready. Almost always a bind to localhost instead of 0.0.0.0. The process is listening, just not anywhere the platform can reach.
The client says it cannot connect. Check that the URL ends in /mcp and that you configured an HTTP transport rather than a stdio command. An entry with a url but no transport type is read as stdio and skipped.
ModuleNotFoundError at startup. A dependency exists in your virtualenv and not in requirements.txt.
A KeyError on boot. A variable you read at import time is not set on the application. Set it and let the redeploy carry it in.
Tool calls are slow. Add logging inside the tool to find which call is slow before you resize anything. It is usually an external API or an unindexed query, not the instance.
Frequently Asked Questions
Can I run an MCP server on serverless?
Yes, and the 2026-07-28 specification made it easier by removing protocol level sessions, so any instance can answer any request. The tradeoff is the cold start: an idle function takes a moment to wake, and that moment lands in front of a person waiting on a tool call. Serverless suits cheap, bursty, read-only tools. Interactive ones are better on something always running.
Does an MCP server need to stay running?
Over stdio, no. The client launches it and it exits when the client does. Over HTTP, yes: nothing will start the process for you, so it has to be listening before the first request arrives, with a restart policy for crashes. A hosted server that sleeps between calls trades cost for a delay on every first call.
How do I authenticate a remote MCP server?
The specification models the server as an OAuth 2.1 resource server: implement Protected Resource Metadata (RFC 9728), return 401 with a WWW-Authenticate challenge, and validate that the token was issued for your server as its audience. For an internal, team only server, a long random bearer token compared in constant time is a reasonable first step. Never put the credential in the URL.
What does it cost to host an MCP server?
The compute for one small always on instance, plus whatever your tools consume. There is no free plan on Out Plane. Starter is $9 a month and Pro is $29, both opening with a 14 day free trial, and each includes an amount of usage in the base fee. Compute is metered by the minute, and SSL, custom domains, logs, monitoring and bandwidth are included. Current figures are on pricing.
Can I connect Claude Desktop to a remote server?
Yes, as a custom connector rather than a hand edited config file. Add it under Customize → Connectors with the server URL. The important detail is that Claude connects from Anthropic's cloud infrastructure, not from your machine, so the server has to be reachable over the public internet. A server on a private network or behind a VPN will not connect unless you allowlist Anthropic's published IP ranges.
Do I need a custom domain?
No. The platform URL is a working HTTPS endpoint with a valid certificate from the moment the app is ready. A custom domain is worth adding when the URL goes into documentation or into other people's client configuration, because it survives renames and platform changes. Mapping one is a CNAME record and an automatic certificate.
Getting it live
Hosting an MCP server is not complicated, but it is a real deployment: an HTTP transport, a process that stays up, TLS, and an authentication check you actually wrote. The hosting choice mostly decides how much of that you maintain yourself. A VPS gives you the lowest bill and the longest list of chores. Serverless gives you the lowest idle cost and a cold start in front of every quiet period. A container platform gives you a build pipeline, certificates and a process that is always listening.
If you want the last of those, push the three files above to GitHub, create the application in the console, set your token as an environment variable, and point your client at /mcp. Starter and Pro both begin with a 14 day free trial, and every plan includes SSL, custom domains, logs and egress.
If you are still deciding what to build, what an MCP server is covers the protocol itself, building an MCP server walks through the code in more depth, and MCP server examples shows what other people have shipped.



