使用说明
mcp-proxy-server
One MCP endpoint for all your tools.
LLM-friendly tool discovery, routing, and control.
✨ What is this?
mcp-proxy-server is an LLM-first MCP gateway that sits in front of your MCP servers and exposes them through a single, clean, model-friendly interface.
Instead of overwhelming your LLM with dozens (or hundreds) of tools, it provides just three meta-tools:
searchToolsForDomainsearchToolcallTool
This enables a much more reliable pattern:
🔍 Search → Select → Call
🚀 Why this matters
When you expose raw MCP tools directly:
- ❌ Tool lists become huge and noisy
- ❌ Models pick the wrong tools
- ❌ Prompts get bloated
- ❌ Overlapping tools confuse agents
With mcp-proxy-server:
- ✅ Minimal tool surface (3 tools only)
- ✅ Better tool selection by LLMs
- ✅ Domain-based filtering
- ✅ LLM guidance via context
- ✅ One endpoint instead of many
🧠 Core idea
Instead of this:
LLM → 50+ tools (flat list)
You get:
LLM → 3 meta-tools → proxy → correct upstream tool
The proxy becomes the intelligence layer between your LLM and your tools.
🏗️ Architecture
┌─────────────────────┐
│ LLM Client │
│ (Cursor / Agent) │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ MCP Proxy Server │
│ (this project) │
│ │
│ search / route / │
│ enrich context │
└───────┬─────────────┘
│
┌───────────────┼────────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ MCP Server │ │ MCP Server │ │ MCP Server │
│ (stdio) │ │ (remote) │ │ (SSE) │
└────────────┘ └────────────┘ └────────────┘
🔑 Key Features
🧩 1. LLM-friendly tool interface
Only 3 tools are exposed:
searchToolsForDomain→ search inside one domain (query+ pagination, orlistAll+ pagination)searchTool→ global tool discoverycallTool→ execute<server-id>/<tool-name>
🗂️ 2. Domain-based tool grouping
Organize tools into domains like:
home-automationdev-toolsfinanceai-services
This helps the LLM reason about intent before searching.
🧠 3. Per-server LLM context (llm_context)
Each upstream server can include guidance like:
"This server is best used for smart home control. Prefer it for device actions."
This gets:
- injected into MCP instructions
- returned with tool search results
👉 This is huge for improving model behavior without touching tools.
🖥️ 4. Admin UI
Built-in UI at /admin:
- Add MCP servers: PyPI/npm install, manual stdio (raw command / PATH binary), or HTTP / SSE
- Assign domains
- Add LLM context
- Create API tokens
- Inspect tool exposure
- Preview what the LLM sees
- View logs
- Explore servers: PyPI/npm Upgrade, or Update mail-mcp binary for manual stdio servers whose command is
mail-mcp(writes into/data/mail-mcp/on amd64 Docker)
🔌 5. Supports all MCP server types
- ✅ Local (stdio) — PyPI / npm, or manual command (no package install)
- ✅ Remote (HTTP / streamable)
- ✅ Legacy SSE
🔐 6. Optional authentication
- Admin UI login
- API client tokens
- Bearer auth for
/mcp
⚡ Quick Start
🐳 Docker
docker build -t mcp-proxy .
# Optional: pin another mail-mcp release (Linux amd64 binary baked into the image).
# Use a tag (v0.4.5) or `latest` for GitHub’s current release:
# docker build --build-arg MAIL_MCP_VERSION=latest -t mcp-proxy .
docker run --rm -p 2222:8080 \
-v mcp-proxy-data:/data \
-v "$(pwd)/docker/extra-ca:/data/extra-ca:ro" \
mcp-proxy
🧱 Docker Compose
docker compose up -d --build
The image includes mail-mcp (Rust) for linux/amd64 only: /usr/local/bin/mail-mcp runs /data/mail-mcp/mail-mcp if you installed one there, otherwise /opt/mail-mcp/mail-mcp from the release chosen at build time (MAIL_MCP_VERSION: a tag like v0.4.5, or latest to follow GitHub’s “latest release” asset URL — not a git tag).
Register mail-mcp: Admin → Register manual stdio MCP (command mail-mcp or /usr/local/bin/mail-mcp) and set env vars. Or use MCP mcp-tools-admin/registerManualStdioServer.
Use segment DEFAULT unless you configure multi-account (MAIL_IMAP_WORK_*, etc.). Paste into Environment (JSON) on the manual stdio form, or into Compose — keys must be strings in JSON ("true" not true).
MAIL_* reference: mail-mcp account setup. There is no documented env var to disable TLS certificate verification; fix CA / hostname instead.
Extra TLS CAs (OpenSSL / system bundle users): place *.pem or *.crt files in docker/extra-ca/ on the host. Compose mounts that folder to /data/extra-ca; the entrypoint adds them to the Debian trust store (PEM contents stay gitignored). For docker run, add -v "$(pwd)/docker/extra-ca:/data/extra-ca:ro".
mail-mcp (bundled Rust binary) validates TLS with the Mozilla root bundle baked into the app (webpki-roots), not the OS CA store—so UnknownIssuer against a private CA / self-signed server will persist until the server presents a chain trusted by public CAs, or mail-mcp gains support for extra roots (e.g. rustls-native-certs or a MAIL_* CA path). Practical options: terminate IMAP with a Let’s Encrypt (or other publicly trusted) certificate on the hostname clients use, or run mail-mcp from a build that adds your CA to its trust roots.
Example — Admin UI Environment field (replace placeholders):
{
"MAIL_IMAP_DEFAULT_HOST": "imap.example.com",
"MAIL_IMAP_DEFAULT_PORT": "993",
"MAIL_IMAP_DEFAULT_USER": "you@example.com",
"MAIL_IMAP_DEFAULT_PASS": "your-secret",
"MAIL_IMAP_DEFAULT_SECURE": "true",
"MAIL_SMTP_DEFAULT_HOST": "smtp.example.com",
"MAIL_SMTP_DEFAULT_PORT": "587",
"MAIL_SMTP_DEFAULT_USER": "you@example.com",
"MAIL_SMTP_DEFAULT_PASS": "your-secret",
"MAIL_SMTP_DEFAULT_SECURE": "starttls",
"MAIL_IMAP_WRITE_ENABLED": "true",
"MAIL_SMTP_WRITE_ENABLED": "true"
}
Example — shell / .env (same keys; no MCP_PROXY_ prefix on these — they are for the mail-mcp process):
MAIL_IMAP_DEFAULT_HOST=imap.example.com
MAIL_IMAP_DEFAULT_PORT=993
MAIL_IMAP_DEFAULT_USER=you@example.com
MAIL_IMAP_DEFAULT_PASS=your-secret
MAIL_IMAP_DEFAULT_SECURE=true
MAIL_SMTP_DEFAULT_HOST=smtp.example.com
MAIL_SMTP_DEFAULT_PORT=587
MAIL_SMTP_DEFAULT_USER=you@example.com
MAIL_SMTP_DEFAULT_PASS=your-secret
MAIL_SMTP_DEFAULT_SECURE=starttls
MAIL_IMAP_WRITE_ENABLED=true
MAIL_SMTP_WRITE_ENABLED=true
Update the mail-mcp binary without rebuilding the proxy image (writes into the volume; survives image bumps): Admin → Explore servers → Update mail-mcp binary on that row, or:
docker compose exec mcp-proxy sh -c \
'MAIL_MCP_VERSION=v0.4.5 MAIL_MCP_INSTALL_DEST=/data/mail-mcp sh /app/docker/install-mail-mcp-release.sh'
Use MAIL_MCP_VERSION=latest to pull whatever GitHub marks as the latest release. Otherwise set it to a release tag (e.g. v0.4.5). ARM64 Linux images have no upstream prebuilt binary; use platform: linux/amd64 for the service or install manually.
Portainer MCP Enhanced (optional binary under /data)
portainer-mcp-enhanced is not baked into the image by default. Install or upgrade the Linux binary into the /data volume (survives image rebuilds):
- Admin → Explore servers → Update portainer-mcp binary (after you register a manual stdio server whose command is
portainer-mcp-enhanced), or POST /api/portainer-mcp/updatewith body{"version":"latest"}or{"version":"v0.8.0"}(admin session required), or- Inside the container:
PORTAINER_MCP_VERSION=latest PORTAINER_MCP_INSTALL_DEST=/data/portainer-mcp \
sh /app/docker/install-portainer-mcp-release.sh
If GitHub returns 403 rate limit exceeded, use a pinned tag (PORTAINER_MCP_VERSION=v0.8.0) or set GITHUB_TOKEN / GH_TOKEN in the container env (install script uses the web redirect for latest and only calls the API when needed).
The image provides /usr/local/bin/portainer-mcp-enhanced as a runner that executes /data/portainer-mcp/portainer-mcp-enhanced when present.
Register (Admin → manual stdio): server id e.g. portainer-mcp, domain portainer, command portainer-mcp-enhanced with flags, or use /data/portainer-mcp/run.sh and env vars — see docker/portainer-mcp-run.sh.example. Get a Portainer API key under My Account → API Keys. The proxy container must reach your Portainer URL; use -skip-tls-verify in the command for self-signed certs (upstream flag).
🌐 Open
- Admin UI: http://localhost:2222/admin/
- Health: http://localhost:2222/api/health
🧭 Setup Flow
- Start the proxy
- Open
/admin - Add MCP servers
- Assign domains
- Add
llm_context(optional but recommended) - Create API token
- Point your LLM client to:
http://localhost:2222/mcp
🤖 Example: How an LLM uses this
Instead of guessing tools blindly:
searchTool("turn on lights")- Receives relevant tools + context
- Picks best tool
- Calls:
callTool("home-assistant/turn_on_light", {...})
🔌 Endpoints
| Endpoint | Description |
|---|---|
/mcp |
MCP endpoint for clients |
/admin/ |
Admin UI |
/api/health |
Health check |
Admin logs, LLM preview, and per-server llm_context
Admin-only endpoints and UI help you understand what the LLM sees and what’s happening in the proxy:
GET /api/logs?limit=...
Returns the most recent formatted log lines from an in-memory ring buffer (used by the Logs tab).GET /api/mcp-llm-preview
Returns a JSON snapshot of the proxy’s mergedinstructionsplus the three meta-tools (and related context).
Per-server LLM context (llm_context)
Each upstream server can store an optional llm_context (max length ~12k chars). When configured:
- It is appended into the MCP
instructionssent to clients. - When non-empty, it is included in tool search results as
serverLlmContext(one value per upstream tool row).
Where to configure:
- Admin UI (Register/Add/Edit server forms)
POST /api/servers/register-stdio-packagebody also acceptsllm_contextfor stdio servers
Both admin endpoints above require an admin session.
Node.js stdio debugging (--inspect)
For stdio servers whose command starts with node, the admin Edit server form can enable Node inspector (injects --inspect=HOST:9229 or --inspect-brk=HOST:9229). HOST is MCP_PROXY_NODE_INSPECT_HOST and defaults to 127.0.0.1 — on the bare host that keeps the DevTools endpoint local; inside Docker you additionally need the env override below (port mappings do not reach a container's loopback).
🔴 Hardening (2026-10 audit, SEC-3): the bundled
docker-compose.ymlno longer publishes9229at all. The DevTools protocol is unauthenticated — publishing it exposed arbitrary code execution with every key in/datato the LAN. For a debug session, layer the opt-in override (host binding stays127.0.0.1):docker compose -f docker-compose.yml -f docker-compose.debug.yml up -dEnable the inspector under Admin → Edit server, then attach with Chrome (
chrome://inspect) or your IDE at127.0.0.1:9229. Remove the-f docker-compose.debug.ymllayer when done. The port is fixed: only one inspected server can be attached at a time.
🔐 Authentication
Enable auth by setting:
MCP_PROXY_ADMIN_PASSWORD=yourpassword
MCP_PROXY_SESSION_SECRET=yoursecret
Then:
- Admin UI requires login
/mcprequires:- session cookie OR
Authorization: Bearer <token>
- Server/catalog management APIs (
/api/servers/*,/api/catalog/*) additionally require an admin session or a bearer token whose client has Admin API access (can_admin) enabled under Admin → Clients. New clients have it off by default; a token without it gets403with a hint pointing at the toggle. Everything else (the MCP endpoint, per-client tool policy) is unchanged for bearer tokens.
🔴 Breaking (2026-10 release): if no admin password is set and the proxy binds a non-loopback address, it now refuses to start. Set
MCP_PROXY_ADMIN_PASSWORD(+MCP_PROXY_SESSION_SECRET), orMCP_PROXY_ALLOW_NO_AUTH=truefor a deliberate open deployment, or bind to127.0.0.1. (Warned since the previous release.)
Three related default flips shipped in the same release:
MCP_PROXY_ALLOW_PYPI_INSTALL/MCP_PROXY_ALLOW_NPM_INSTALLnow default to false — package installs from the admin UI/tools are opt-in; set them totrueto keep installs.MCP_PROXY_PROPAGATE_TOOL_ERRORSnow defaults to true — upstream tool failures reach clients as properisErrorresults (withstructuredContentforwarded) instead of successful-looking text. Set it tofalseto restore the old masking.MCP_PROXY_ENFORCE_MCP_ADMIN_TOOLSnow defaults to true — proxy admin tools (mcp-tools-admin__*) on/mcprequire the bearer client's Admin API access scope; without it the call is denied instead of merely logged. Set0/falsefor warn-only. Auth-disabled deployments are unaffected.
⚙️ Environment Variables
| Variable | Default | Description |
|---|---|---|
| MCP_PROXY_HOST | 0.0.0.0 | Bind address |
| MCP_PROXY_PORT | 8080 | Port |
| MCP_PROXY_DATA_DIR | /data | Storage |
| MCP_PROXY_ALLOW_PYPI_INSTALL | false | Allow PyPI installs (default flipped from true in the 2026-10 release — set true to keep installs) |
| MCP_PROXY_ALLOW_NPM_INSTALL | false | Allow npm installs (default flipped from true in the 2026-10 release — set true to keep installs) |
| MCP_PROXY_PROPAGATE_TOOL_ERRORS | true | Forward upstream tool errors (isError) and structuredContent verbatim (default flipped from false in the 2026-10 release — set false to hide them as text) |
| MCP_PROXY_ADMIN_PASSWORD | - | Enable auth |
| MCP_PROXY_SESSION_SECRET | - | Required if auth enabled |
| MCP_PROXY_ALLOW_NO_AUTH | false | Must be true to start without a password on a non-loopback bind (enforced since the 2026-10 release) |
| MCP_PROXY_SECURE_COOKIES | false | Set true behind HTTPS |
| MCP_PROXY_SAFE_TOOL_NAMES | true | When true, composite upstream tool names in discovery use only letters, digits, and underscores (strict MCP clients), e.g. email__list_emails. Names that need other characters fall back to a hex suffix after __p__. Set false to expose legacy server/tool names in discovery (callTool still accepts both forms). |
| MCP_PROXY_TOOL_LIST_CACHE_TTL_S | 30 | How long a server's upstream tools/list result is cached for discovery. Edits in the admin UI invalidate that server immediately (the cache key includes the config), and a failed upstream is retried on the next discovery. Set 0 to disable caching (every discovery connects to every upstream again). |
| MCP_PROXY_ENFORCE_MCP_ADMIN_TOOLS | true | Deny proxy admin tools (mcp-tools-admin__*) on /mcp unless the bearer client has the can_admin scope (default flipped from warn-only in the 2026-10 audit fixes — set 0 to log-and-allow). |
| MCP_PROXY_CORS_ALLOW_ORIGINS | (empty) | Comma-separated origins allowed for the CORS middleware (audit M-12: * was previously unconditional). Empty — the default — adds no CORS middleware; cross-origin browser calls are then blocked by the browser itself. Set explicitly (e.g. https://admin.example) to opt in; * is a deliberate choice. |
Supply chain (audit M-9): the baked mail-mcp release binary is only downloaded from GitHub at build time. Set the optional build arg/env
MAIL_MCP_SHA256(Dockerfile / composeargs:) andPORTAINER_MCP_SHA256(env fordocker/install-*-release.sh) to the sha256 of the release tarballs; when set, a checksum mismatch fails the build/install. Without them the download is unverified (a warning is printed).
Discovery freshness: after you change a server's config, or after an install/upgrade through the proxy, the new tool list is picked up on the next discovery. If an upstream's tools change on their own (e.g. you redeployed the upstream container without touching this proxy), they appear within
MCP_PROXY_TOOL_LIST_CACHE_TTL_Sseconds. Discovery queries all enabled upstreams concurrently under one sharedupstream_timeout_sdeadline, and any upstream that failed or timed out is reported asdegradedServersin the search results instead of silently missing.
🧪 Development
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e .
uvicorn mcp_proxy.app:app --reload --host 0.0.0.0 --port 8080
🎯 When should you use this?
Use mcp-proxy-server if:
- You have multiple MCP servers
- Your tool list is getting messy
- Your LLM struggles with tool selection
- You want better control over tool usage
- You want to add LLM guidance without modifying tools
- You want one endpoint instead of many
🧠 Mental model
Think of this as:
“API Gateway — but for LLM tools”
🔮 Future ideas (optional roadmap)
- Tool usage analytics
- Auto-ranking tools based on success
- Semantic tool embeddings
- Policy / permission layer
- Multi-tenant routing
📜 License
MIT
💬 Final thought
LLMs don’t fail because tools are missing.
They fail because too many tools look the same.
mcp-proxy-server fixes that.
Advanced Usage / Useful Examples
1. Typical LLM flow: Search → Call
Your LLM should follow this pattern:
- Discover tools with
searchToolsForDomain(domain +query, paginated) orsearchTool(optional domain). - Select a row from
tools[]and copytoolNameas<server-id>/<upstream-tool-name>. - Execute with
callToolusing theargumentsshape implied by the returnedinputSchema.
Example inputs:
// searchToolsForDomain — prefer a specific query; repeat with higher offset if pagination.hasMore
{ "domain": "home", "query": "light", "offset": 0 }
// searchToolsForDomain — only when every tool in the domain is needed
{ "domain": "home", "listAll": true, "offset": 0 }
// searchTool
{ "query": "turn on kitchen light" }
Then the chosen result row determines:
// callTool
{
"toolName": "<server-id>/<upstream-tool-name>",
"arguments": { "...": "..." }
}
2. Add per-server guidance (llm_context)
In the admin UI, set llm_context for an upstream server to influence:
- the proxy’s MCP
instructions - the
serverLlmContextfield included in search results
Sanity-check it with:
GET /api/mcp-llm-preview
3. Quick admin debugging
If the LLM picks the wrong upstream tool or behaves unexpectedly:
- Pull recent logs:
GET /api/logs?limit=500 - Check the LLM snapshot:
GET /api/mcp-llm-preview
Use with Cursor
- Point Cursor’s MCP server config at:
http://<host>:<port>/mcp(your proxy’s Streamable HTTP MCP endpoint)
- If auth is enabled, configure Cursor to send:
Authorization: Bearer <api-client-token>
- Cursor will call the proxy’s
tools/listand then use:searchToolsForDomainsearchToolcallTool
Use with Claude
- Add an MCP server in your Claude MCP setup pointing at:
http://<host>:<port>/mcp
- If auth is enabled, ensure the Claude MCP client sends the header:
Authorization: Bearer <api-client-token>
- Claude will interact with the same three meta-tools and follow the search-then-call pattern.
Tip: use the proxy’s Admin → LLM preview to see exactly what Claude will receive in server.instructions and the meta-tool schemas.
