remote-mcp
by suhasdesai02
README.md
# Remote MCP Server — HTTP transport + authentication
A minimal MCP server exposed over the network (HTTP) instead of stdio,
with bearer token authentication — the two things that change the
moment an MCP server leaves your own laptop.
This project is intentionally simple (two trivial tools: `echo` and
`get_server_time`) so the lesson is entirely about **transport and
auth mechanics**, not new business logic — you already know RAG, tool
use, and MCP fundamentals from Projects 2-5.
## The core concept
Every previous project used **stdio transport**: Claude Code *launches*
your script as its own child process and talks to it over stdin/stdout.
Only whoever can start a process on your machine can ever reach it —
there's no meaningful "who's allowed to call this" question, because
nothing outside your machine can even see it.
**HTTP transport is fundamentally different**: the server runs as an
independent, long-lived network service, reachable by URL. Once that's
true, *anyone* who can reach the port can send it requests — so
authentication stops being optional.
```python
# stdio (every previous project) — implicitly private, no auth needed
mcp.run()
# HTTP (this project) — network-reachable, auth now required
mcp.run(transport="http", host="0.0.0.0", port=8000)
```
## Verified live: the actual authentication boundary
This was tested against a real running server process, not simulated —
three real HTTP requests, three real outcomes:
```
--- Attempt with NO auth token ---
Correctly rejected: HTTPStatusError: Client error '401 Unauthorized'
--- Attempt with WRONG auth token ---
Correctly rejected: HTTPStatusError: Client error '401 Unauthorized'
--- Attempt with CORRECT auth token ---
Tools discovered: ['get_server_time', 'echo']
Echo result: Server received: hello from a real HTTP client
Server time: 2026-08-13T22:07:47.826826
```
Notice the wrong-token case: it fails identically to no-token-at-all —
proof the server is genuinely validating the token's *value*, not just
checking that some header is present.
## Setup
```bash
pip install -r requirements.txt
```
```powershell
$env:REMOTE_MCP_TOKEN = "pick-any-secret-string-here"
```
## Run it locally
```bash
python remote_server.py
```
You'll see it start a real Uvicorn web server, listening at
`http://0.0.0.0:8000/mcp` — this is a genuinely different kind of
process than every previous project's script, which just ran, did
something, and exited.
## Connect a client (from a second terminal, while the server is running)
```python
import asyncio
from fastmcp import Client
async def main():
async with Client("http://localhost:8000/mcp", auth="pick-any-secret-string-here") as client:
result = await client.call_tool("echo", {"message": "hello"})
print(result.data)
asyncio.run(main())
```
## Connecting Claude Code to a remote HTTP server
Unlike previous projects (`claude mcp add name -- python script.py`,
which tells Claude Code to *launch* the process), an HTTP server is
already running independently — you point Claude Code at its URL
instead:
```bash
claude mcp add remote-demo --transport http https://your-deployed-url.com/mcp
```
(Exact syntax for passing the bearer token alongside this may vary by
Claude Code version — check `claude mcp add --help` for the current
auth-header flag.)
## Actually deploying this off your laptop
Running `python remote_server.py` locally proves the mechanism works,
but it's still only reachable from your own machine (`localhost`). To
make it genuinely reachable by others, you need to run it on a machine
that's always on and has a public address — a small cloud host, not
your laptop.
**Simplest free options for a small Python service like this:**
- **Render** (render.com) — free tier, connects directly to a GitHub
repo, auto-deploys on push. Set `REMOTE_MCP_TOKEN` as an environment
variable in their dashboard (never commit it to the repo).
- **Railway** (railway.app) — similar free-tier flow, GitHub-connected.
- **Fly.io** — free tier, more control but a bit more setup (a
`fly.toml` config file, in addition to your requirements.txt).
All three follow roughly the same shape: connect your GitHub repo →
they detect it's Python → they run something equivalent to
`python remote_server.py` on a real server with a real public URL →
you set `REMOTE_MCP_TOKEN` as a secret in their dashboard, not in code.
**This step is deliberately left as your next hands-on exercise**,
since the actual mechanics (transport + auth) are already verified
above — picking a host, connecting a GitHub repo, and setting an
environment variable in a web dashboard is a different, more
click-through-y skill than anything code-related, and it's worth doing
yourself rather than following a script blindly.
## Design notes
- **`StaticTokenVerifier` is explicitly for development/testing only**
— tokens are stored in plain text in server memory. A real production
deployment would use `JWTVerifier` against a real identity provider
(Auth0, Okta, your company's SSO), not a single shared secret string.
This project uses the static version deliberately, to isolate the
*concept* (HTTP needs auth) from the *complexity* (real OAuth/JWT
flows) — worth knowing the limitation, not just the pattern.
- **`host="0.0.0.0"` vs `"localhost"` matters.** `localhost` only
accepts connections from the same machine — binding to it would
defeat the purpose of this whole exercise. `0.0.0.0` means "accept
connections on any network interface," which is what actually makes
remote access possible once deployed.
- **The wrong-token test is the important one**, not the no-token test.
A server that merely checks "is *a* token present" without validating
its value would still pass a naive test but be completely insecure —
this project's verification specifically confirms invalid credentials
are rejected, not just missing ones.
## What's next
- Deploy to Render/Railway/Fly.io and re-run the same three-case auth
test against the real public URL instead of `localhost`.
- Swap `StaticTokenVerifier` for `JWTVerifier` against a real identity
provider — the production-grade version of this same concept.
- Point Claude Desktop (not just Claude Code) at the deployed URL, to
confirm the server truly doesn't care which client connects — the
same client-independence property from Project 4, now proven over a
real network instead of a local subprocess.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues