# Deploy your own MCP server with HTTPS and an access key

*Originally published on the [Hubfly space blog](https://blog.hubfly.space/deploy-mcp-server-https-access-key?utm_source=hashnode&utm_campaign=deploy-mcp-server-https-access-key).*

> **TL;DR**
>
> - **What you get:** your own MCP server on a public HTTPS address, locked with an access key, and connected to Claude Code, Cursor and Codex.
> - **Who it's for:** developers who built (or want to build) an MCP server and are tired of it only working on `localhost`.
> - **Time:** about 15 minutes. **Cost:** free on the shared tier (360 compute hours a month, no card); about $2.25 a month always on (dedicated 0.5 vCPU, 512 MB).

Your MCP server works great on your laptop. Then you open Cursor on your other machine, or a teammate asks for it, and it's gone. Local servers die when your laptop sleeps, and you end up copying the same setup to every computer.

In this guide you'll put one MCP server online, behind HTTPS and an access key, and point every AI tool you use at the same address. One server, one place to update it, and nobody else can call your tools.

[**Try it on Hubfly space →**](https://dashboard.hubfly.space) (free tier, no card needed)

## What you'll build

```plaintext
Claude Code ─┐
Cursor ──────┼──► https://your-server.eu1.hubfly.app/mcp ──► access key check ──► your MCP server (Python)
Codex ───────┘                                              (at the edge)         (Hubfly space Cell container)
```

A small Python MCP server with two tools, `add_note` and `list_notes`. Swap in your own tools later; the deploy steps stay the same.

## Step 1: Write the server

Install the official Python SDK on your machine to try it locally first:

```bash
pip install "mcp==2.3.0"
```

Create `server.py`:

```python
import os

from mcp.server import MCPServer

mcp = MCPServer("notes")
notes: list[str] = []


@mcp.tool()
def add_note(text: str) -> str:
    """Save a short note."""
    notes.append(text)
    return f"Saved note #{len(notes)}"


@mcp.tool()
def list_notes() -> list[str]:
    """Return every saved note."""
    return notes


if __name__ == "__main__":
    mcp.run(
        transport="streamable-http",
        host="0.0.0.0",
        port=int(os.environ.get("PORT", "8000")),
    )
```

Two lines matter more than they look:

- **`transport="streamable-http"`** makes the server speak MCP over HTTP instead of stdin/stdout. Remote clients can only reach an HTTP server.
- **`host="0.0.0.0"`** makes it listen on every network interface. The SDK's default is `127.0.0.1`, which only accepts connections from inside the container itself, so the platform's proxy can't reach it and you get a 502 error.

The server answers on `/mcp`, so locally it lives at `http://127.0.0.1:8000/mcp`.

> **Note on the SDK version:** version 2 of the `mcp` package renamed `FastMCP` to `MCPServer`. Many tutorials still use `from mcp.server.fastmcp import FastMCP`, which fails on 2.x. Pin the version so a future release doesn't change things under you.

## Step 2: Add a Dockerfile

Put this `Dockerfile` next to `server.py`:

```docker
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir "mcp==2.3.0"
COPY server.py .
EXPOSE 8000
CMD ["python", "server.py"]
```

Installing the package during the build (not at startup) keeps restarts fast: the container starts the server straight away instead of downloading packages each time.

Push both files to a GitHub repository.

## Step 3: Deploy it on Hubfly space

1. Sign in to the [Hubfly space dashboard](https://dashboard.hubfly.space) and click **Create Project**. Give it a name like `notes-mcp`, pick a region close to you, and (optional) open **Advanced Options** to set a monthly budget cap, for example $10.
2. In the project, click **Create → Create Container**. Name it `mcp-server`.
3. Under **Deployment Source**, choose **Git repository**, connect GitHub through the GitHub App, and pick your repository. A `Dockerfile` in the repository root is used for the build.
4. On **Resources**, keep **Shared Tier (Free)**: 0.3 vCPU and 256 MB RAM, enough for a small server.
5. Click through to **Review** and press **Deploy Container**. The container page shows each deploy stage as it runs.

Open the **Logs** tab. When you see this line, the server is ready:

```plaintext
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
```

## Step 4: Give it a public HTTPS address

Right now the server only exists on the private network. To reach it from your laptop:

1. Open the container's **Network** tab and click **Add Domain**.
2. Keep **Auto Endpoint**. Under **Suggested ports**, the live scan shows **8000 · Open now**. Click it.
3. Leave **TLS / SSL** and **Force HTTPS** on, and click **Create Subdomain**.

The address is named after your container, for example `https://mcp-server.eu1.hubfly.app`. Your MCP endpoint is that address plus `/mcp`.

Check that it answers. This is the MCP "hello" (the `initialize` call):

```bash
curl -i https://<your-address>/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
```

You get `HTTP 200`, an `mcp-session-id` header and a reply that starts like this:

```json
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{...},"protocolVersion":"2025-06-18","serverInfo":{"name":"notes","version":""}}}
```

The round trip takes well under a second once the server is awake.

**Don't stop here.** At this point anyone who finds the address can call your tools. Public addresses get visits from scanners within minutes of being created, so lock it before you connect anything.

## Step 5: Lock it with an access key

Hubfly space can check an access key at the edge, before the request ever reaches your container. Your Python code doesn't change.

1. Go back to the project overview and click the **Firewall** box above your container. This opens **Security &amp; Protection Studio**.
2. Check that your domain is selected in the domain picker, then open the **Identity Access Control** tab.
3. Under **Access Authentication Mode**, choose **API Key Only**, switch **Policy Enforcement State** on, and click **Save Policy Settings**. The status changes from *Permissive* to *Enforced*.
4. Open the **API Keys** tab. Enter a name (for example `laptop-clients`), leave **Allowed Paths** empty, optionally set an expiry date, and click **Generate Secret Key**.
5. Copy the key (it starts with `hfa_live_`). **It is shown only once**, so save it in your password manager right away.

Now run the `curl` from Step 4 again without a key. You get:

```http
HTTP/1.1 401
{"error":"identity_access_required","request_id":"..."}
```

A made-up key gets the same 401. These requests are stopped at the edge: they never show up in your container's logs, so a flood of bad requests doesn't cost your server any work.

To call it with the key, add one header:

```bash
-H "X-HubFly-Access-Key: $MCP_KEY"
```

Keep the key in an environment variable or your password manager, never in a Git repository. Issue one key per person or machine, with an expiry date, so you can retire one without breaking the others.

## Step 6: Connect Claude Code, Cursor and Codex

First, put the key in your shell so it stays out of config files:

```bash
export MCP_KEY="hfa_live_..."   # paste your key
```

**Claude Code** (one command):

```bash
claude mcp add --transport http notes https://<your-address>/mcp \
  --header "X-HubFly-Access-Key: $MCP_KEY"
```

Run `/mcp` inside Claude Code to see the server and its two tools.

**Cursor:** add this to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project):

```json
{
  "mcpServers": {
    "notes": {
      "url": "https://<your-address>/mcp",
      "headers": { "X-HubFly-Access-Key": "${env:MCP_KEY}" }
    }
  }
}
```

**Codex:** add this to `~/.codex/config.toml`:

```toml
[mcp_servers.notes]
url = "https://<your-address>/mcp"
env_http_headers = { "X-HubFly-Access-Key" = "MCP_KEY" }
```

`env_http_headers` reads the value from the `MCP_KEY` environment variable (use `http_headers` instead for a fixed value). The key name matters: Codex's setting is `http_headers`/`env_http_headers`, not the `headers` you use in Cursor, and a request without the key gets a 401.

Ask your client to list its MCP tools; you should see `add_note` and `list_notes`. Then try "add a note: buy oat milk" followed by "list my notes". Every client you connect uses the same server, so they share the same notes. If a client reports 401, the header name or key is wrong.

**What about claude.ai and Claude Desktop connectors?** Custom connectors there support OAuth or no auth. Fixed headers are a limited beta for some organizations, and a custom header name like `X-HubFly-Access-Key` needs approval from Anthropic first. For those apps, plan on OAuth.

## What can go wrong

**1. `421 Misdirected Request` with "Invalid Host header".** This happens if you run the server with Uvicorn directly (`uvicorn.run(mcp.streamable_http_app(), ...)`) instead of `mcp.run(...)`. In that mode the SDK turns on DNS-rebinding protection and only accepts `Host: 127.0.0.1` or `localhost`, so every request through your public address is refused. Fix it by allowing your address:

```python
from mcp.server.transport_security import TransportSecuritySettings

app = mcp.streamable_http_app(
    transport_security=TransportSecuritySettings(
        allowed_hosts=["mcp-server.eu1.hubfly.app"]  # your address
    )
)
```

Note that once you set `allowed_hosts`, local requests to `127.0.0.1` are refused with the same 421, so add `"127.0.0.1:8000"` to the list if you also run it locally. The `mcp.run(transport="streamable-http", host="0.0.0.0")` setup from Step 1 doesn't hit this.

**2. `502 Bad Gateway` from the address.** The server is listening on `127.0.0.1`, usually because `host="0.0.0.0"` is missing. Add it and redeploy. If **Suggested ports** in the Add Domain dialog shows nothing, the server isn't listening yet: check the Logs tab.

**3. The client times out on the first call of the day.** Shared-tier containers suspend after 15 minutes of inactivity, and the first call after a pause can be slow. Some clients give up quickly (Codex waits 10 seconds by default; raise it with `startup_timeout_sec = 30` in the same TOML block). If you use the server all day, move it to the dedicated tier with **Auto Sleep** off.

**Bonus: notes disappear after a redeploy.** This example keeps notes in memory, and everything outside a volume is wiped when the container restarts or redeploys. For real data, use a database (see below). Also keep an eye on memory: this small server already uses most of the shared tier's 256 MB, so heavier tools need a dedicated container.

## What it costs

- **Shared tier:** free within the 360 compute hours a month included in the free tier. The project's budget panel shows a projected **$0.00/month** for this setup.
- **Always on (dedicated, 0.5 vCPU, 512 MB RAM, 5 GB disk, Auto Sleep off):** about **$2.25 a month**.
  - CPU: 0.5 × $0.004 × 730 h = $1.46
  - RAM: 0.5 GB × $0.002 × 730 h = $0.73
  - Disk: 5 GB × $0.00001536 × 730 h = $0.06
- The access key check, the HTTPS address and the certificate are free.

## Takeaway

Getting an MCP server off `localhost` takes four things: speak HTTP (`streamable-http`), listen on `0.0.0.0`, give it an HTTPS address, and check a key before requests reach your code. Do those once and every AI tool you use shares the same tools.

## Go further on Hubfly space

- **Give your tools real data:** add a PostgreSQL database from the templates with a volume so data survives redeploys, and connect to it over the private network. See the [Compose docs](https://www.hubfly.space/docs/compose/) and [Docker health checks that catch real problems](https://blog.hubfly.space/docker-health-checks-postgres-redis?utm_source=hashnode&utm_campaign=deploy-mcp-server-https-access-key) for a Compose setup.
- **Let your team in with their login instead of keys:** switch the route to **Login or API Key** so teammates sign in in the browser while tools keep using keys. See [Identity Access](https://www.hubfly.space/docs/identity-access/).
- **Let your coding agent deploy for you:** the [Hubfly space AI skill](https://www.hubfly.space/ai) teaches Claude Code, Codex and Cursor to deploy and update projects with the `hubfly` CLI.

[**Try it on Hubfly space →**](https://dashboard.hubfly.space) Free tier, 360 compute hours a month, no card.
