使用说明
MCP Proxy
Personal project — Docker Compose setup to expose stdio-only MCP servers as OAuth-authenticated SSE / Streamable HTTP endpoints from a self-hosted VPS.
What it does
Many MCP servers only support stdio transport and cannot be consumed by remote clients directly. This project wraps them with mcp-proxy and exposes them over HTTPS, so any remote MCP client (Claude Desktop, Claude Code, claude.ai, …) can connect without local installations.
Authentication is OAuth 2.1 via Scalekit, implemented by auth_proxy.py — Claude Desktop does not support static bearer tokens for remote MCP servers.
Architecture
Client (Claude Desktop / claude.ai)
→ HTTPS — Traefik (external, TLS)
→ auth_proxy.py :8080 Starlette — OAuth discovery + Scalekit token validation
→ mcp-proxy :9090 (127.0.0.1 only) named-server mode
→ stdio child processes: parcel, trillium, brightdata, inventree
Traefik is not part of this compose file: the container joins a pre-existing external Docker network named traefik and is routed via labels.
Quick start
-
Copy
.env.exampleto.envand fill in the values:cp .env.example .envVariable Description DOMAINHostname pointed at this server (e.g. mcp.example.com)SCALEKIT_ENVIRONMENT_URLScalekit environment URL (e.g. https://you.scalekit.dev)SCALEKIT_CLIENT_IDScalekit client ID SCALEKIT_CLIENT_SECRETScalekit client secret SCALEKIT_RESOURCE_IDScalekit MCP resource ID ( res_…) — must match the token audienceTRACKING_API_TOKENAPI key from 17track.net (parcel) TRILLIUM_API_TOKENTriliumNext ETAPI token (Options → ETAPI → Create Token) BRIGHTDATA_TOKENBright Data API token INVENTREE_TOKENInvenTree API token (Settings → User → API Tokens) All of them are required:
entrypoint.shfails fast on missing server tokens andauth_proxy.pyraises on missingSCALEKIT_*vars. -
Start the stack:
docker compose up -d --build -
Verify:
curl https://$DOMAIN/status # 200, lists configured servers curl https://$DOMAIN/.well-known/oauth-protected-resource # OAuth discovery document curl -si https://$DOMAIN/servers/parcel/sse | head -5 # 401 + WWW-Authenticate
Endpoints
| Path | Transport | Auth |
|---|---|---|
/servers/parcel/sse |
SSE | OAuth 2.1 |
/servers/parcel/mcp |
Streamable HTTP | OAuth 2.1 |
/servers/trillium/sse · /servers/trillium/mcp |
SSE / Streamable HTTP | OAuth 2.1 |
/servers/brightdata/sse · /servers/brightdata/mcp |
SSE / Streamable HTTP | OAuth 2.1 |
/servers/inventree/sse · /servers/inventree/mcp |
SSE / Streamable HTTP | OAuth 2.1 |
/.well-known/oauth-protected-resource |
HTTP | None |
/status |
HTTP | None |
Client configuration
Add the server URL as a remote MCP connector; the client discovers the authorization
server from /.well-known/oauth-protected-resource and runs the OAuth flow itself —
no token to paste.
https://mcp.example.com/servers/parcel/mcp
Configured servers
| Name | Package / target | Config source |
|---|---|---|
parcel |
parcel-mcp (17track) |
config.json written next to index.mjs by entrypoint.sh |
trillium |
@aimbitgmbh/trillium-mcp (TriliumNext Notes) |
per-server env block injected into servers.json |
brightdata |
remote MCP at https://mcp.brightdata.com/mcp, bridged by mcp-remote |
Bearer token injected into the --header arg |
inventree |
syntaxerr66/inventree-mcp (Go binary built in the Dockerfile) |
per-server env block injected into servers.json |
Packages are installed with bun into /opt/mcp-packages (not globally) and launched
as bun /opt/mcp-packages/node_modules/<pkg>/….
Adding a new MCP server
-
Add an entry to
servers.json(top-level key ismcpServers):{ "mcpServers": { "new-server": { "command": "bun", "args": ["/opt/mcp-packages/node_modules/new-mcp-server/dist/index.js"] } } } -
Add the package to the
bun addline in theDockerfile. -
Wire up its configuration:
- env vars → add them to the container in
docker-compose.yml, then inject them into that server'senvblock inservers.jsonfromentrypoint.sh. mcp-proxy runs without--pass-environment, so stdio children do not inherit the container environment (see below). - config file → generate it in
entrypoint.sh(see the parcel case below). - remote HTTP MCP → bridge it with
mcp-remote, following thebrightdataentry.
- env vars → add them to the container in
-
Add secrets to
.envand.env.example. -
docker compose up -d --build
Gotchas
Child processes don't inherit container env. mcp-proxy runs without
--pass-environment, so its base_env is empty. A server reading FOO_TOKEN from the
environment gets undefined unless it has a per-server env block in servers.json.
This is deliberate: it keeps the Scalekit secrets out of every child process. The MCP SDK
still merges get_default_environment() (PATH, HOME, …), so bun stays resolvable.
config.json path. parcel-mcp reads its config from
path.join(__dirname, "config.json") — the file must sit next to index.mjs inside the
package directory, not in the working directory. entrypoint.sh writes it to
/opt/mcp-packages/node_modules/parcel-mcp/config.json at container startup. Any server
with the same behaviour needs the same treatment.
Scalekit audience. The token audience must be the resource ID (res_…), not the
public URL.
SSE streaming. Starlette's BaseHTTPMiddleware buffers responses and breaks SSE —
auth_proxy.py uses raw ASGI middleware instead.
Historical docs
docs/superpowers/ contains the original design spec and implementation plan from
April 2026. They describe the pre-OAuth architecture (Traefik traefik-plugin-apikey
bearer auth, npm global installs) and are kept only as a record — this README and
CLAUDE.md are the current documentation.
