Deploy MCP Proxies
MCP servers communicate via stdio (stdin/stdout), but browsers need HTTP endpoints. This tutorial shows you how to deploy the proxies that bridge the gap, locally with Docker or in production with systemd and nginx.
Deploy MCP proxies to make public MCP servers (NASA, HackerNews, Wikipedia, etc.) accessible from the browser.
Prerequisites
Section titled “Prerequisites”- Docker and Docker Compose installed (for the quick method)
- Or: an Ubuntu/Debian VM with
python3,node,npm,nginx(for production) - Understanding of what an MCP server is (see Connect an MCP server)
What you will build
Section titled “What you will build”MCP servers accessible via HTTP, with CORS configured, recipes injected, and systemd monitoring.
graph LR Browser[Browser] -->|HTTPS| Nginx[nginx reverse proxy] Nginx -->|HTTP localhost:900x| Bridge["mcp-stdio-bridge.py"] Bridge -->|stdin/stdout| MCP["MCP Server (npx/python3)"]Architecture
Section titled “Architecture”Each bridge is a Python process that:
- Spawns the MCP server as a child process communicating via stdio
- Accepts
POST /mcpwith a JSON-RPC 2.0 body - Forwards to the subprocess via stdin, reads the response from stdout
- Manages the subprocess lifecycle (lazy start, restart on crash)
- Optionally injects recipe tools from a JSON file
graph TB subgraph "Per MCP server" HTTP["POST /mcp (JSON-RPC 2.0)"] --> Bridge["mcp-stdio-bridge.py"] Bridge --> Stdin["stdin"] Stdin --> Process["MCP server subprocess"] Process --> Stdout["stdout"] Stdout --> Bridge Bridge --> Response["HTTP Response"] end
subgraph nginx CORS["CORS Headers"] --> Proxy["proxy_pass localhost:900x"] end
Client[McpClient] --> nginx nginx --> HTTPQuick method: Docker
Section titled “Quick method: Docker”For local development, Docker Compose starts all servers in one command:
cd mcp-proxiesdocker compose up -dAll servers are then accessible on localhost:9001 through localhost:9008.
To start a single server:
docker compose up -d hackernewsCheckpoint: test with curl:
curl -s -X POST http://localhost:9006/mcp \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { "name": "test", "version": "1.0" } } }'A valid response contains "result" with the server capabilities.
Production method: systemd + nginx
Section titled “Production method: systemd + nginx”For production on an Ubuntu/Debian VM:
cd mcp-proxiessudo ./setup.shThe script is idempotent (safe to re-run). It performs:
- Prerequisite checks (
python3,node,npm,nginx) - Creates a dedicated
mcpbridgesystem user - Copies bridge scripts to
/opt/mcp-bridge/ - Generates systemd unit files for each server
- Generates nginx location blocks
- Starts or restarts all services
- Tests each endpoint with curl
sequenceDiagram participant Admin participant Setup as setup.sh participant System as systemd participant Nginx as nginx
Admin->>Setup: sudo ./setup.sh Setup->>Setup: Check prerequisites Setup->>Setup: Create mcpbridge user Setup->>Setup: Copy scripts to /opt/mcp-bridge/ Setup->>System: Generate + enable systemd units Setup->>Nginx: Generate locations + reload Setup->>Setup: curl test each endpoint Setup-->>Admin: Startup reportNASA API key
Section titled “NASA API key”The NASA proxy requires a NASA_API_KEY environment variable:
export NASA_API_KEY="your-key-here"sudo -E ./setup.shAdding a new MCP server
Section titled “Adding a new MCP server”1. Create the directory
Section titled “1. Create the directory”mkdir mcp-proxies/servers/my-server2. Add configuration files
Section titled “2. Add configuration files”mcp-proxies/servers/my-server/ recipes.json # Recipe tools injected into the server (optional) service.conf # systemd configuration (command, port, env) README.md # Server documentation examples.md # Prompt examples3. Create recipes (optional but recommended)
Section titled “3. Create recipes (optional but recommended)”[ { "name": "search-guide", "description": "How to search in my-server", "content": "## Search\\n\\nUse search({query: 'keyword'})..." }]4. Re-run setup
Section titled “4. Re-run setup”sudo ./setup.shCheckpoint: the new service appears in systemctl status mcp-bridge-my-server.
Testing a server
Section titled “Testing a server”Send an initialize request:
curl -s -X POST http://localhost:9001/mcp \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { "name": "test", "version": "1.0" } } }'Then list tools:
curl -s -X POST http://localhost:9001/mcp \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} }'Connecting from a webmcp-auto-ui app
Section titled “Connecting from a webmcp-auto-ui app”import { McpClient } from '@webmcp-auto-ui/core';
// Local developmentconst client = new McpClient('http://localhost:9001/mcp');
// Productionconst client = new McpClient('https://demos.hyperskills.net/mcp-metmuseum/mcp');
await client.initialize();const tools = await client.listTools();For multiple servers:
import { McpMultiClient } from '@webmcp-auto-ui/core';
const multi = new McpMultiClient();await multi.addServer('https://demos.hyperskills.net/mcp-metmuseum/mcp');await multi.addServer('https://demos.hyperskills.net/mcp-wikipedia/mcp');Recipe system
Section titled “Recipe system”Each server can optionally include a recipes.json file that injects recipe tools (list_recipes, get_recipe, search_recipes) into the MCP server’s tool list. The bridge loads these at startup via the --recipes flag.
Recipes let the LLM discover how to use the server autonomously, even if the original MCP server has no built-in discovery mechanism.
Available servers
Section titled “Available servers”| Server | Main tools | Port | Production URL | Example prompt |
|---|---|---|---|---|
| hackernews | get-front-page, search-posts | 9006 | /mcp-hackernews/mcp | ”Top 10 stories on HN today” |
| metmuseum | search-museum-objects, get-museum-object | 9001 | /mcp-metmuseum/mcp | ”Show impressionist paintings” |
| openmeteo | weather_forecast, geocoding | 9002 | /mcp-openmeteo/mcp | ”Weather in Paris this week” |
| wikipedia | search, readArticle | 9005 | /mcp-wikipedia/mcp | ”Article about quantum computing” |
| inaturalist | search_observations | 9007 | /mcp-inaturalist/mcp | ”Birds spotted near Lyon” |
| nasa | nasa_apod, nasa_mars_rover, nasa_neo | 9008 | /mcp-nasa/mcp | ”Latest Mars rover photos” |
| datagouv | remote proxy (no bridge) | — | /mcp-datagouv/mcp | ”Transport datasets in France” |
Production URLs are prefixed with https://demos.hyperskills.net.
File structure
Section titled “File structure”mcp-proxies/ bridge/ mcp-stdio-bridge.py # Generic stdio-to-HTTP bridge inaturalist-mcp.py # Custom Python MCP server for iNaturalist nginx/ mcp-locations.conf # nginx location blocks (CORS + proxy_pass) servers/ hackernews/ # Per-server config and recipes metmuseum/ openmeteo/ wikipedia/ inaturalist/ nasa/ datagouv/ setup.sh # VM provisioning script docker-compose.yml # Docker alternative for local devTroubleshooting
Section titled “Troubleshooting”| Problem | Likely cause | Solution |
|---|---|---|
| ”Connection refused” | Bridge not started | systemctl status mcp-bridge-<name> |
| ”CORS error” | Missing CORS headers in nginx | Check mcp-locations.conf |
| Timeout on first call | MCP server takes time to start | Bridge does lazy start, first call is slow |
| ”Invalid JSON-RPC” | Wrong request format | Check JSON-RPC 2.0 body format |
| Server crash loop | Missing npm dependency | journalctl -u mcp-bridge-<name> for logs |
Going further
Section titled “Going further”- Create a custom MCP server: write your own MCP server in Node.js or Python
- Monitoring: add systemd healthchecks and alerts
- Security: add token authentication to endpoints