Scaffold a production-ready remote MCP (Model Context Protocol) server with OAuth, RFC 8414/9728 discovery, retry, telemetry, and tests — in one command. Zero runtime dependency on this package.
Project description
remote-mcp
One command to a production-ready remote MCP server.
Building a remote MCP (Model Context Protocol) server means solving the same hard problems every time: OAuth pass-through, RFC 8414/9728 discovery endpoints, middleware stack, telemetry, retry logic, Docker packaging, and a test suite that passes on day one.
remote-mcp scaffolds all of it. Generated projects have zero runtime dependency on this package — every file is yours to read, audit, and modify.
Install
pipx install remote-mcp # recommended
# or
pip install remote-mcp
Usage
Interactive:
remote-mcp new my-project
Non-interactive (CI / scripts):
remote-mcp new my-project --yes --service-name "My Project" --into ./apps/my-project
FastMCP Remote Server Generator
Scaffolding my-project into my-project...
✓ Scaffold complete
Done! Next steps:
cd my-project
python -m venv venv && source venv/bin/activate
pip install -e ".[dev]"
cp env.example .env
uvicorn asgi:application --reload --port 8001 --lifespan on
Server is live at http://localhost:8001. MCP endpoint: http://localhost:8001/mcp.
Also works as a module:
python -m remote_mcp new my-project
Commands
| Command | What it does |
|---|---|
remote-mcp new <name> |
Scaffold a new project |
remote-mcp add tool <name> |
Add a tool stub to an existing scaffolded project |
remote-mcp add openapi <SPEC> |
Generate typed MCP tools from an OpenAPI 3.x spec — requires pip install 'remote-mcp[openapi]' |
remote-mcp add database <DSN> |
Generate read-only MCP tools from a live database schema — requires pip install 'remote-mcp[db]' |
remote-mcp doctor |
Report drift between sources.lock.json and reality |
remote-mcp --version |
Print version |
new flags:
| Flag | Purpose |
|---|---|
--yes / -y |
Skip prompts; use defaults / provided flags |
--service-name / -s "Foo Bar" |
Human-readable service name |
--into PATH |
Target directory (default ./<name>) |
--auth-mode none|passthrough|jwt |
Default auth mode for the generated project (default: passthrough) |
--github-owner OWNER |
GitHub user/org for the MCP registry namespace in server.json |
--legacy-sse |
Also serve the deprecated SSE transport at /sse |
add tool flags:
| Flag | Purpose |
|---|---|
--project-dir / -p PATH |
Target project (default .) |
See Sources below for add openapi, add database, and doctor flags.
What you get
my-project/
├── src/
│ ├── server.py # FastMCP("My Project") instance + tool mounts
│ ├── app.py # Starlette factory — routes, middleware, ASGI wiring
│ ├── config/settings.py # Pydantic BaseSettings (lazy get_settings())
│ ├── core/
│ │ ├── auth.py # extract_bearer_token() — OAuth pass-through
│ │ ├── errors.py # MyProjectError hierarchy
│ │ ├── http_client.py # api_get/post/patch/put/delete/upload + tenacity retry
│ │ ├── telemetry.py # JSONL event log, HMAC-salted user-id hashing
│ │ ├── handlers.py # @tool_handler decorator, get_client_and_token()
│ │ └── app_state.py # lifespan: shared httpx.AsyncClient pool
│ ├── middleware/
│ │ ├── auth.py # RequireAuthMiddleware — Bearer enforcement + probe
│ │ └── telemetry.py # TelemetryMiddleware — auth failures, connections
│ ├── views/
│ │ ├── health.py # GET /health
│ │ ├── oauth.py # RFC 8414 + RFC 9728 discovery endpoints
│ │ └── root.py # landing page at /
│ └── tools/example.py # echo tool — mounted and live at first boot
├── tests/
│ ├── test_auth.py # extract_bearer_token edge cases
│ ├── test_middleware.py # auth bypass, 401 on missing token
│ └── test_telemetry.py # hash_token stability, record_event never raises
├── templates/index.html # landing page served at /
├── Dockerfile # multi-stage, non-root, healthcheck
├── docker-compose.yml # local dev parity
├── render.yaml # Render Blueprint — one-click deploy
├── fly.toml # Fly.io app config
├── mcp.json # MCP Inspector preset
├── server.json # MCP Registry metadata
├── .github/
│ └── workflows/
│ └── publish-mcp.yml # registry publish on version tags
├── .dockerignore
├── .gitignore
├── asgi.py # production ASGI entrypoint
├── env.example # all env vars with safe defaults
├── pyproject.toml # hatchling build, ruff lint
├── README.md
└── DEPLOYMENT.md # Render / Railway / Fly.io / Docker deploy guide
Included infrastructure
| Module | What it provides |
|---|---|
core/auth.py |
extract_bearer_token(ctx) — forward Bearer token verbatim to your backend |
core/http_client.py |
api_get, api_post, api_patch, api_put, api_delete, api_upload — pooled httpx client with tenacity retry |
core/errors.py |
MyProjectError + Auth, Forbidden, Validation, Backend, RateLimit subclasses |
core/telemetry.py |
Rotating JSONL log, user IDs hashed via HMAC-SHA256 (TELEMETRY_HASH_SALT) |
core/handlers.py |
@tool_handler — catches errors, formats responses, records telemetry |
middleware/auth.py |
CORS → Auth → optional backend token probe on MCP connect (reuses pooled client) |
middleware/telemetry.py |
Connection-level events (auth failures, rate limits, MCP connects) |
views/ |
Health, OAuth discovery (RFC 8414 + 9728), landing page — each in its own file |
Dockerfile |
Multi-stage build, non-root app user, healthcheck, WORKERS env |
Nothing is forced on you. Delete what you don't need.
Adding a tool
Quickest path:
remote-mcp add tool search
Then mount it in src/server.py:
from src.tools.search import search_router
mcp.mount(search_router)
Or write one by hand:
# src/tools/my_tool.py
from fastmcp import Context, FastMCP
from src.core.handlers import get_client_and_token, tool_handler
my_router = FastMCP("my-tool")
@my_router.tool()
@tool_handler
async def my_tool(param: str, ctx: Context) -> str:
client, auth_header = await get_client_and_token(ctx)
response = await client.get("/some/endpoint", headers={"Authorization": auth_header})
return response.json()
Sources
Generate tools from what you already have — an OpenAPI spec or a database — instead of writing them by hand.
From an OpenAPI spec
pip install 'remote-mcp[openapi]'
remote-mcp add openapi https://api.example.com/openapi.json --tag orders
| Flag | Purpose |
|---|---|
--project-dir / -p PATH |
Target project (default .) |
--name TEXT |
Source name / package dir (default: derived from spec title) |
--include GLOB (repeatable) |
Select operations by operationId glob |
--exclude GLOB (repeatable) |
Drop operations by operationId glob |
--tag TAG (repeatable) |
Select whole OpenAPI tag(s) |
--yes / -y |
Non-interactive; requires --include or --tag |
On a TTY without selection flags, an interactive numbered picker (e.g. 1,3-5 or all) opens (operations grouped by tag). Each selected operation becomes one typed tool under src/tools/<name>/, with a generated test file alongside it. The command prints the exact lines to add to src/server.py to mount the new router — same pattern as add tool.
From a database
pip install 'remote-mcp[db]'
remote-mcp add database postgresql://user:pass@host/db --include "orders*"
| Flag | Purpose |
|---|---|
--project-dir / -p PATH |
Target project (default .) |
--include GLOB (repeatable) |
Select tables by name glob |
--exclude GLOB (repeatable) |
Drop tables by name glob |
--schema NAME |
DB schema (driver default if unset) |
--allow-write TABLE:OP (repeatable) |
Opt in to a write tool; OP is insert or update |
--yes / -y |
Non-interactive; requires --include |
API tools act as the caller; database tools act as the server. OpenAPI tools forward the caller's Bearer token, so the backend enforces the caller's own permissions. Database tools run with the server's own DATABASE_URL credentials — anyone who can call the MCP server can read everything the allowlist exposes. That's why tools are read-only by default: an empty allowlist exposes nothing, and every write is a separate, individually named, opt-in tool (insert_orders, update_orders_by_id, ...) enabled only via --allow-write TABLE:OP. There is no generic run_sql tool and there never will be — a free-form query tool is a prompt-injection exfiltration pump.
The DSN is used only at generation time, to introspect the schema — it is never written to any file. The generated src/tools/db/db.py reads DATABASE_URL (plus DB_MAX_ROWS, default 200, and DB_STATEMENT_TIMEOUT_MS, default 5000) from the environment at runtime. add database appends these three keys to env.example (only if the file exists and doesn't already have them) as a reminder to set them in the deploy environment.
Staying current
Every add openapi / add database run writes sources.lock.json at the project root: what was selected, the spec/schema state it came from, and a content hash per generated file. remote-mcp doctor reads it back and reports:
| Check | Needs --refresh |
|---|---|
| Manifest present and parseable | no |
Generator version vs. installed remote-mcp |
no |
| Generated files present / locally modified | no |
| Spec/schema drift vs. the live source | yes (network/DB access) |
remote-mcp doctor --project-dir . --refresh --json
Exit codes: 0 clean, 1 drift found, 2 error. --json emits machine-readable output for CI. Credential-bearing DSNs (postgres, mysql) can't be re-checked live — the manifest only ever stores a sanitized DSN with credentials stripped — so --refresh reports those sources unreachable rather than guessing; sqlite and other credential-free DSNs refresh normally. To pick up real drift, re-run the same add command: it's idempotent per source name and regenerates the files without touching anything outside the generated set.
How this differs from FastMCP's runtime OpenAPI integration
FastMCP can proxy an OpenAPI spec at runtime — no generation step, but also no code you can read, patch, or grep. remote-mcp add openapi generates code you own: typed tool functions with generated tests, deployed through the same rail as the rest of the scaffold, with zero runtime coupling back to either the spec or this package. Nothing calls out to re-parse the spec on every request; you regenerate when you choose, and you can audit every line remote-mcp ever wrote.
Connecting to Claude
Claude.ai (web): Settings → Connectors → Add → Custom → Web
- URL:
https://your-server.example.com/mcp
Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"my-project": {
"command": "npx",
"args": ["-y", "mcp-remote@latest", "https://your-server.example.com/mcp"]
}
}
}
Claude Code:
claude mcp add my-project --transport http https://your-server.example.com/mcp
Local debugging with MCP Inspector
The scaffold ships an mcp.json preset:
npx @modelcontextprotocol/inspector --config ./mcp.json
Set a Bearer token in the Inspector UI to exercise authenticated tools without standing up a full OAuth flow. For zero-config local dev (no token needed), run with AUTH_MODE=none.
Deployment
Docker / Compose
docker build -t my-project .
docker run --rm -p 8001:8001 --env-file .env my-project
# or
docker compose up --build
One-click platforms
The generated project ships pre-filled config:
- Render —
render.yamlBlueprint; click the Deploy to Render button in your repo README. - Fly.io —
fly.toml; runfly launch --copy-config && fly deploy. - Railway — Dockerfile-based; point Railway at the repo.
DEPLOYMENT.md in the generated project covers env vars, workers, healthcheck, and hardening notes.
Publish to the MCP Registry
Every scaffolded project ships server.json (MCP Registry metadata) and .github/workflows/publish-mcp.yml (OIDC-authenticated publish workflow). To list your server on the official registry:
- Edit
server.json: replaceYOUR-DOMAINwith your deployed domain and setgithub_ownerto your GitHub user/org. - Ensure the server is publicly reachable at the URL in
remotes[].url. - Push a version tag:
git tag v1.0.0 && git push --tags.
The workflow authenticates via GitHub OIDC (no stored secrets) and publishes to registry.modelcontextprotocol.io, which feeds the discovery catalogues that MCP clients use.
Configuration
Copy env.example to .env and edit. Key variables:
| Variable | Default | Description |
|---|---|---|
API_BASE_URL |
https://api.example.com |
Your upstream API |
AUTH_MODE |
passthrough |
none | passthrough | jwt — auth enforcement mode |
OAUTH_JWKS_URL |
`` | JWKS URL, required when AUTH_MODE=jwt |
JWT_AUDIENCE |
`` | JWT audience; defaults to MCP_PUBLIC_URL |
MCP_PUBLIC_URL |
http://localhost:8001/mcp |
Public MCP URL (shown in landing page) |
OAUTH_ISSUER_URL |
http://localhost:8001 |
OAuth issuer for RFC 8414 discovery |
OAUTH_AUTHORIZE_PATH |
/o/authorize/ |
OAuth authorize endpoint (relative or full URL) |
OAUTH_TOKEN_PATH |
/o/token/ |
OAuth token endpoint |
OAUTH_REGISTRATION_PATH |
/o/register/ |
Dynamic client registration endpoint |
OAUTH_SCOPES_SUPPORTED |
read |
Comma-separated scope list |
AUTH_PROBE_ENABLED |
false |
Validate token against backend on MCP connect |
AUTH_PROBE_PATH |
/health/ |
Endpoint used for token probe |
LOGO_URI |
`` | Logo shown in OAuth discovery (optional) |
ALLOWED_ORIGINS |
https://claude.ai,... |
CORS origins; unset = all origins accepted (startup warning emitted) |
TELEMETRY_ENABLED |
true |
Enable JSONL telemetry |
TELEMETRY_HASH_SALT |
`` | HMAC salt for user-id hashing (set per-deployment) |
SUPPORT_EMAIL |
`` | Shown as Get Help mailto link on the landing page; hidden when unset |
Full variable list and deploy instructions in DEPLOYMENT.md.
How it works
remote-mcp renders Jinja2 templates into your project directory at scaffold time. After that, it's gone — no version pinning, no update command, no hidden runtime. You own every line.
Requirements
- Python ≥ 3.10 (this CLI). Generated project supports the same range.
- FastMCP ≥ 3.0 (installed in the generated project, not this package).
Contributing
See CONTRIBUTING.md. Security reports: SECURITY.md.
Changelog
See CHANGELOG.md.
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file remote_mcp-1.0.0.tar.gz.
File metadata
- Download URL: remote_mcp-1.0.0.tar.gz
- Upload date:
- Size: 71.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3fd8f3cb84374ef98c1b8e274f543930c7635932f3ed827d9562d1a678605d2f
|
|
| MD5 |
4ebcc59a5450a4a1145a30132db8cc7c
|
|
| BLAKE2b-256 |
248590a4d33e378f16ee5bbb40fc0c9140717318d60bcae5a175dc9fc811d7ea
|
Provenance
The following attestation bundles were made for remote_mcp-1.0.0.tar.gz:
Publisher:
publish.yml on pushpendra-tripathi/remote-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
remote_mcp-1.0.0.tar.gz -
Subject digest:
3fd8f3cb84374ef98c1b8e274f543930c7635932f3ed827d9562d1a678605d2f - Sigstore transparency entry: 2136971843
- Sigstore integration time:
-
Permalink:
pushpendra-tripathi/remote-mcp@ea69f5affd62a985a86392002043de9d3b4361e2 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/pushpendra-tripathi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ea69f5affd62a985a86392002043de9d3b4361e2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file remote_mcp-1.0.0-py3-none-any.whl.
File metadata
- Download URL: remote_mcp-1.0.0-py3-none-any.whl
- Upload date:
- Size: 77.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f987a2a7292782354383fb93dfbe1ed307e080ea310361875ecd5cf11477a092
|
|
| MD5 |
5ce020812dc413b5d6adf1012a444f1e
|
|
| BLAKE2b-256 |
1b63024609734a0686c81b5f90c75c2e6e113ebe7d7df49b93048d93c535c23e
|
Provenance
The following attestation bundles were made for remote_mcp-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on pushpendra-tripathi/remote-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
remote_mcp-1.0.0-py3-none-any.whl -
Subject digest:
f987a2a7292782354383fb93dfbe1ed307e080ea310361875ecd5cf11477a092 - Sigstore transparency entry: 2136971880
- Sigstore integration time:
-
Permalink:
pushpendra-tripathi/remote-mcp@ea69f5affd62a985a86392002043de9d3b4361e2 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/pushpendra-tripathi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ea69f5affd62a985a86392002043de9d3b4361e2 -
Trigger Event:
push
-
Statement type: