MCP工具代理网关

mcp-proxy-server 是一个面向 LLM 的 MCP 网关,通过三个元工具提供统一的工具搜索、选择与调用接口,帮助模型在多个 MCP 服务器中高效发现并调用合适的工具。

  • by holger81
  • Python
  • 混合部署
  • 更新于
在 GitHub 查看 holger81/mcp-proxy-server

使用说明

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:

  • searchToolsForDomain
  • searchTool
  • callTool

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, or listAll + pagination)
  • searchTool → global tool discovery
  • callTool → execute <server-id>/<tool-name>

🗂️ 2. Domain-based tool grouping

Organize tools into domains like:

  • home-automation
  • dev-tools
  • finance
  • ai-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/update with 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

  1. Start the proxy
  2. Open /admin
  3. Add MCP servers
  4. Assign domains
  5. Add llm_context (optional but recommended)
  6. Create API token
  7. Point your LLM client to:
http://localhost:2222/mcp

🤖 Example: How an LLM uses this

Instead of guessing tools blindly:

  1. searchTool("turn on lights")
  2. Receives relevant tools + context
  3. Picks best tool
  4. 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 merged instructions plus 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:

  1. It is appended into the MCP instructions sent to clients.
  2. 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-package body also accepts llm_context for 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.yml no longer publishes 9229 at all. The DevTools protocol is unauthenticated — publishing it exposed arbitrary code execution with every key in /data to the LAN. For a debug session, layer the opt-in override (host binding stays 127.0.0.1):

docker compose -f docker-compose.yml -f docker-compose.debug.yml up -d

Enable the inspector under Admin → Edit server, then attach with Chrome (chrome://inspect) or your IDE at 127.0.0.1:9229. Remove the -f docker-compose.debug.yml layer 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
  • /mcp requires:
    • 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 gets 403 with 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), or MCP_PROXY_ALLOW_NO_AUTH=true for a deliberate open deployment, or bind to 127.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_INSTALL now default to false — package installs from the admin UI/tools are opt-in; set them to true to keep installs.
  • MCP_PROXY_PROPAGATE_TOOL_ERRORS now defaults to true — upstream tool failures reach clients as proper isError results (with structuredContent forwarded) instead of successful-looking text. Set it to false to restore the old masking.
  • MCP_PROXY_ENFORCE_MCP_ADMIN_TOOLS now defaults to true — proxy admin tools (mcp-tools-admin__*) on /mcp require the bearer client's Admin API access scope; without it the call is denied instead of merely logged. Set 0/false for 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 / compose args:) and PORTAINER_MCP_SHA256 (env for docker/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_S seconds. Discovery queries all enabled upstreams concurrently under one shared upstream_timeout_s deadline, and any upstream that failed or timed out is reported as degradedServers in 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:

  1. Discover tools with searchToolsForDomain (domain + query, paginated) or searchTool (optional domain).
  2. Select a row from tools[] and copy toolName as <server-id>/<upstream-tool-name>.
  3. Execute with callTool using the arguments shape implied by the returned inputSchema.

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 serverLlmContext field 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

  1. Point Cursor’s MCP server config at:
    • http://<host>:<port>/mcp (your proxy’s Streamable HTTP MCP endpoint)
  2. If auth is enabled, configure Cursor to send:
    • Authorization: Bearer <api-client-token>
  3. Cursor will call the proxy’s tools/list and then use:
    • searchToolsForDomain
    • searchTool
    • callTool

Use with Claude

  1. Add an MCP server in your Claude MCP setup pointing at:
    • http://<host>:<port>/mcp
  2. If auth is enabled, ensure the Claude MCP client sends the header:
    • Authorization: Bearer <api-client-token>
  3. 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.