mcp-plugin — an MCP server in front of external MCP servers
mcp-plugin is itself a Streamable HTTP MCP server. It connects to external MCP servers — stdio, SSE, or Streamable HTTP, no SDK required on their side — keeps those connections alive, and exposes everything to a host agent as MCP tools on one endpoint. Every feature is an MCP tool: add/configure/remove servers, search and load their tools, call them, check connection status. Any MCP client can drive it.
Ships with Slife as its built-in MCP plugin
but has no dependency on it. Depends only on fastmcp, mcp, httpx,
json5, and aiosqlite; credstore is optional (for ${PLACEHOLDER} secret
resolution and OAuth token storage).
external MCP servers (stdio / SSE / streamable http)
npx anyapi-mcp-server ────┐
@modelcontextprotocol/… ──┤ persistent connections
your own server ──────────┤
▼
┌───────────────────────────────────┐
host agent ─┤ mcp-plugin │
(any MCP │ Streamable HTTP MCP server │ connect via
client) │ http://127.0.0.1:<port>/mcp │ {"port": N}
mcp_tool_… └───────────────────────────────────┘
Tools
All management lives in the server's tool set:
| Tool | What it does |
|---|---|
mcp_set |
Add or update an external server connection (upsert). stdio via command + args, or http via url. Persists to mcp-plugin.json5. |
mcp_set_enabled |
Runtime enable/disable of a server (mcp_set_enabled(name, enabled)): enable reconnects + loads tools, disable disconnects + unloads. |
mcp_remove |
Remove a server: stop its process, unregister its tools, persist the removal. |
mcp_list |
List configured servers (transport, command/url, enabled). |
__mcp_connection_status |
Live per-server status: running/stopped, tool counts, errors. |
mcp_list_tools(server) |
List a server's tools — double read of the live connection AND the persisted catalog (mcp-plugin.db). |
mcp_tool_search(query, mode, limit, server, include_disabled) |
Search the tool catalog — hybrid / fts5 / grep (see Semantic search & automatic degradation). |
__mcp_get_tool(full_name) |
One tool's live schema + enabled status (consumed by the host's mcp_tool_load). |
__mcp_call_tool(full_name, arguments) |
Call a tool on a connected server (invoked by the host via per-tool proxies). |
The management tools are kept separate from the servers they manage. Tool loading on the host side is on-demand by default — see On-demand vs auto-load.
Run
The server supports two serving modes — pick by how your client learns the URL:
| Mode | Command | Client knows the URL how? |
|---|---|---|
| Fixed port (standalone) | python -m mcp_plugin.server --port 8123 |
Statically — http://127.0.0.1:8123/mcp |
| Auto-assigned (default / slife plugin) | python -m mcp_plugin.server |
From the stdout signal {"port": N} |
A client that must point a static URL at this server needs the fixed mode
(the same reason slife's embeddings provider fixes local-embed's :8000). A
host that spawns the process and discovers it — slife, or any supervisor —
uses auto-assigned: the server binds a free port, then signals {"port": N}
to stdout as a single JSON line once it is ready to serve MCP.
python -m mcp_plugin.server --port 8123 # fixed port, standalone
python -m mcp_plugin.server # auto-assigned (+ {"port": N} signal)
Both modes still emit the {"port": N} signal (fixed mode reports the same
port); the server always binds 127.0.0.1.
As a slife external plugin (the usual hosting), slife spawns
python -m mcp_plugin.server itself, reads the port signal, connects on
behalf of the agent, and exports MCP_PLUGIN_FILE so the plugin reads
mcp-plugin.json5 from next to slife.json5:
plugins: { external: [ { name: "mcp-plugin", module: "mcp_plugin.server" } ] }
Install
Requires uv. The uv routes below pin
Python 3.13 (requires-python is >=3.13, but 3.13 is what CI tests, and
uv would otherwise pick the newest 3.14 it finds).
Your external MCP servers need their own runtimes too: each servers.<name>
entry spawns its command: as a subprocess, so the command must be installed
and on PATH (see the config example below). Typical ones are npx
(Node.js), bun / bunx (Bun), and uvx (uv). A server whose command is
missing fails at connect time with an "executable not found" error.
Optional: credstore (mcp-plugin[credstore]) — only needed when a
secret uses a ${PLACEHOLDER} that isn't in your shell env, or you use OAuth
(auth: {type: "oauth"}). Without it, placeholders resolve against the shell
env only, and OAuth fails with a clear error naming the extra.
uv tool install --python 3.13 mcp-plugin # standalone
uv tool install --python 3.13 'mcp-plugin[credstore]' # + credential store (secret placeholders, OAuth)
# or bundled with Slife (its built-in MCP gateway):
uv tool install --python 3.13 git+https://github.com/juzcn/slife.git
pip install mcp-plugin (into a Python 3.13 environment) also works when uv
isn't available. Re-running a uv tool install over an existing install
is a no-op unless you pass --reinstall — use it to upgrade to a new release.
Config: mcp-plugin.json5
The server reads and writes mcp-plugin.json5, located by precedence:
MCP_PLUGIN_FILE=<path>(env var — Slife exports this to the same directory asslife.json5when it launches the plugin)./mcp-plugin.json5(dev — when the current directory is the Slife source root, i.e. itspyproject.tomlhasproject.name == "slife")~/.mcp-plugin/mcp-plugin.json5(default, standalone use)
A missing file is treated as first run (empty config — the server runs with
no servers and keyword-only search); it is only created on the first write.
A file that exists but cannot be parsed raises ConfigParseError rather than
being overwritten.
This is the same resolution credstore uses for its credentials.crypt.
Secret values — env entries, auth.client_id/client_secret, HTTP
headers, and the embeddings api_key — accept three forms: "" (empty: no
value / no Authorization header), plaintext (used as-is), or a ${VAR}
placeholder resolved at use time in order shell env → credstore → literal
(VAR is both the env-var name and the credstore key). The embeddings
api_key specifically treats an unresolvable placeholder as empty — it never
sends a literal Bearer ${VAR} — while other fields keep the unresolved
${VAR} literal as the last resort. ${VAR} refs may also appear embedded
in args and url (e.g. "--header", "Authorization: Bearer ${GITHUB_TOKEN}").
// mcp-plugin.json5
{
servers: {
filesystem: {
enabled: false, // optional; absent = enabled
command: "npx",
args: [
"-y",
"@modelcontextprotocol/server-filesystem",
".",
],
},
github: {
auto_load: true, // optional; absent/false = on-demand tools
command: "npx",
args: [
"-y",
"anyapi-mcp-server",
"--name", "github",
"--spec", "https://api.github.com/github-raml",
"--base-url", "https://api.github.com",
"--header", "Authorization: Bearer ${GITHUB_TOKEN}",
],
},
remote: {
url: "https://example.com/mcp",
headers: { Authorization: "Bearer ${REMOTE_TOKEN}" },
auth: { type: "oauth", client_id: "${OAUTH_CLIENT_ID}" },
},
},
// Optional — semantic tool search. Present + base_url ⇒ active; absent ⇒
// mcp_tool_search falls back to keyword/grep. Point it at any OpenAI-
// compatible /v1 endpoint (the local-embed plugin serves this).
embeddings: {
base_url: "http://127.0.0.1:8000/v1",
api_key: "local", // optional; "" | plaintext | "${VAR}" (env → credstore)
model: "bge-m3", // optional; endpoint's active model used otherwise
},
}
The command/args pairs above are spawned as subprocesses on this machine,
so install the runtime each command needs (npx → Node.js, bun/bunx →
Bun, uvx → uv); see Install.
On-demand vs auto-load
External MCP tools are on-demand by default: the host does not bulk-register
them into the LLM's tool list. The agent discovers them with mcp_tool_search,
loads one into the tool list with mcp_tool_load, and releases them at server
granularity (mcp_remove / mcp_set_enabled(false)).
Set auto_load: true on a server to keep the old behavior — its tools are
bulk-registered whenever the server connects.
Enable/disable is always batch at the server level (mcp_set_enabled) —
there is no per-tool toggle. A disabled server's tools are indexed but marked
disabled in the catalog, refused at call time, and skipped by mcp_tool_load.
Semantic search & automatic degradation
mcp_tool_search defaults to mode="hybrid" — it merges semantic hits with
keyword hits only when the embeddings section is configured and the
backend is actually working. In every other situation it degrades
automatically to keyword search (fts5 BM25, with a LIKE substring
fallback for CJK) — search never fails because embeddings is absent or
broken:
- embeddings not configured (no
embeddingssection) →fts5 - embeddings misconfigured (
base_urlplaceholder, wrong endpoint, bad auth) →fts5 - embeddings endpoint unreachable at search time (was reachable at
startup, is not now) →
fts5 - embedding model failed to load →
fts5 - semantic index still building/rebuilding →
fts5, upgrading back tohybridautomatically when indexing finishes
mode="grep" (exact substring) never involves embeddings and always works.
The result's mode field reports what actually ran (hybrid / fts5 /
grep) and a hint names the reason, so a caller can always tell search
degraded. Enable semantic search by fixing the embeddings section and
running mcp-plugin build (there is no MCP tool for it).
Tool catalog (mcp-plugin.db)
Every connected server's tools are indexed into a SQLite DB (mcp-plugin.db,
next to the config, or $MCP_PLUGIN_DB): full_name ({server}__{tool}),
name, description, and a per-tool enabled flag. The catalog persists
across restarts, so mcp_tool_search works before any reconnect. Changing
enabled persists too; a disabled tool is refused at call time.
mcp_list_tools is a double read: the live connected tools AND the
persisted catalog. When the catalog is unavailable, or its data is out of
date vs the live tools (single source of truth = live), the response flags it
and suggests running mcp-plugin build offline to rebuild the catalog. While
a server is disconnected, the persisted catalog (if any) is still returned; a
failed live read instead returns an error ("MCP unavailable") without touching
the catalog. mcp_tool_load(full_name) (host side) registers one tool into
the LLM's tool list and refuses disabled tools.
CLI
The CLI is auxiliary — it covers what the MCP tools do not. Server
management stays in the tools (mcp_set, mcp_remove, mcp_set_enabled).
| Command | Description |
|---|---|
mcp-plugin |
Overview of configured servers |
mcp-plugin set-embed --base-url <url> [--model <name>] [--api-key <key>] |
Add/update the embeddings section (semantic search) |
mcp-plugin build |
Rebuild the tool catalog DB + index from live connections |
set-embed writes/updates the top-level embeddings section: --base-url is
required (a placeholder \${…} or empty value leaves semantic search
disabled, keyword fallback only). --model and --api-key (alias
--apikey) are optional — omit one to keep its current value, pass ""
to clear it. The api_key accepts the same forms as other secrets ("" /
plaintext / ${VAR}) and is stored verbatim. Changes apply at the next
server start, or run mcp-plugin build to (re)index now.
Build
mcp-plugin build reconnects every enabled server, re-syncs its tools into the
catalog, rebuilds the FTS index, and (when an embeddings section is present)
re-embeds the whole catalog. Use it after hand-editing mcp-plugin.json5,
after an external MCP server updates its tools, or after switching the
embeddings model. Unreachable servers are reported, not fatal.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_plugin-0.1.3.tar.gz.
File metadata
- Download URL: mcp_plugin-0.1.3.tar.gz
- Upload date:
- Size: 101.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a423615b4d94dcd3c9f4c37b5794b6a6e705e4e6ef420d1cd25f5fb136c07c6b
|
|
| MD5 |
24139c5beee702793d9e8905f504784a
|
|
| BLAKE2b-256 |
8962d57891700d57580a83f858974f095908a33be1c0419448af515b3614e5e5
|
File details
Details for the file mcp_plugin-0.1.3-py3-none-any.whl.
File metadata
- Download URL: mcp_plugin-0.1.3-py3-none-any.whl
- Upload date:
- Size: 85.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
011da6778cf8978f08003c4c9d76cf4a52245483fb9aaa8b9fa6c02fe229bc5c
|
|
| MD5 |
085a3011431142ab69ea9324a35d3cad
|
|
| BLAKE2b-256 |
30b67a24c767a07cb1b3ab600f2ffb50a99c52a4e1aa817d88dcdb51ca7b61fc
|