mcpproxy: A Config-Driven MCP Host with a Built-In Web UI
Published:
The Model Context Protocol defines a standard way for AI clients to discover and call tools, but standing up a personal MCP server still usually means writing Python glue code, wiring up a framework, and restarting a process every time you add a tool. mcpproxy takes a different approach: every tool provider is a single YAML file, the server reloads tools at startup without any code changes to the host, and a browser-based web UI handles the full provider lifecycle — editing, secret management, and live command streaming — without leaving the browser.
Experimental software — use with caution and in isolation.
mcpproxyis a research prototype provided as-is, with no guarantees of security, stability, or fitness for any particular purpose. It has not undergone a security audit. The web UI has no authentication. Do not expose it to untrusted networks or use it to process sensitive data in production. Run it on a trusted network, preferably in a container or VM you are prepared to reset. See the LICENSE for full MIT terms.
How It Works
mcpproxy exposes two ports: the MCP endpoint on 8888 (http://localhost:8888/mcp) and a web UI on 8889 (http://localhost:8889). The core server (server.py) scans a tools/ directory at startup. Each YAML file there is a provider. A provider either embeds Python async def handler functions directly in a code: block, or delegates to an existing MCP npm package via an npx: block. server.py executes each code block (or spawns the npx process), registers every declared tool automatically, and serves them all through the single MCP endpoint — no changes to server.py ever needed.
A minimal Python provider looks like this:
code: |
import datetime
from typing import Any
async def ping(context: dict[str, Any], message: str = "hello") -> dict[str, Any]:
return {
"ok": True,
"echo": message,
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
}
tools:
- name: ping
function: ping
description: Echo a message back with a server-side UTC timestamp.
input_schema:
type: object
properties:
message:
type: string
default: "hello"
description: The text to echo back.
required: []
An npx-based provider is even shorter — just point at the package:
npx:
command: npx @playwright/mcp@latest --headless --isolated
tools:
- name: browser_navigate
description: Navigate to a URL in a browser.
input_schema:
type: object
properties:
url:
type: string
required: [url]
Secret Injection
Secrets are declared in the provider YAML and injected from the environment at call time. The key design property is that secret values are never part of the MCP tool schema — the LLM never sees them:
tools:
- name: get_weather
function: get_weather
...
secrets:
env:
api_key: WEATHER_API_KEY # handler arg → env var name
The server reads WEATHER_API_KEY from .env (loaded via Docker Compose env_file) and passes it to the handler as the api_key argument. The LLM’s tool schema only shows the public parameters.
The Web UI
A FastAPI frontend on port 8889 handles the full provider lifecycle.
Tools tab — lists all loaded providers in a left panel. Click any provider to open a form editor with its documentation, code, and per-tool fields (name, description, parameters). Add or remove tools with the + Add Tool / ✕ buttons, save to disk, and restart the MCP server in place — no shell access needed.
+ New Provider wizard — choose between a Python code provider (write async def functions) and an npx package provider (enter an npx command; the UI auto-introspects the MCP server and populates tool definitions). The wizard’s final step lists all required secrets and writes them to .env directly.
🔑 Secrets panel — reads all secrets.env entries from the selected provider, shows which variables are already set in .env, and lets you fill in or update missing values interactively.
🛠 Run Command — runs any shell command inside the server environment and streams output live. Particularly useful for npx-based providers: after adding a Playwright provider, install the Chrome binary with npx playwright install chrome right from the browser panel.
Connecting a Client
The MCP endpoint is http://localhost:8888/mcp. Most major clients support the HTTP transport natively:
# Claude Code
claude mcp add --transport http mcpproxy http://localhost:8888/mcp
For Claude Desktop, Cursor, Cline, Continue, OpenCode, and Windsurf, add a JSON server entry pointing at the same URL. For Ollama (which does not speak MCP natively), the included tests/ollama_agent.py bridges MCP → Ollama tool-calling automatically:
python3 tests/ollama_agent.py "List the tools you have available"
Docker
A pre-built image is published to the GitHub Container Registry on every push to main:
docker pull ghcr.io/billjr99/mcpproxy:latest
docker run -d --rm \
-p 8888:8888 -p 8889:8889 \
--env-file .env \
-v "$(pwd)/tools":/app/tools \
--name mcpproxy \
ghcr.io/billjr99/mcpproxy:latest
The tools/ directory is gitignored and never baked into the image — it is always mounted at runtime so your provider files stay outside the container. For a persistent home directory setup that also lets the web UI’s Secrets panel read and write .env, bind-mount ~/.mcpproxy/.env into the container and pass -e MCP_ENV_FILE=/app/.env.
A docker-compose.override.yml is provided for local development with bind mounts; the base docker-compose.yml uses named volumes for production/CI.
Getting Started
The fastest path is run_local.sh:
git clone https://github.com/BillJr99/mcpproxy
cd mcpproxy
./run_local.sh
The script generates .env.example from any existing tool YAMLs, prompts for missing secret values, creates a virtualenv, installs dependencies, and starts both the MCP server and the web UI. Then open http://localhost:8889, click + New Provider, and add your first tool.
The unit test suite covers server.py helpers and all frontend/app.py endpoints and runs on every push via GitHub Actions:
pip install -r requirements.txt -r requirements-dev.txt
pytest tests/ -v
Source code and further documentation are at https://github.com/BillJr99/mcpproxy.
