mcp-plugin-gateway
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
mcp-plugin-gateway
A plugin system where each plugin is a bare FastMCP server. The system discovers installed plugins, mounts them on an aggregate MCP server, and generates a matching command-line interface from the same tool schemas. A plugin author writes ordinary FastMCP code and declares one entry point; nothing else is required.
The distribution and command are named mcp-plugin-gateway. The Python import
package is mcp_plugin_gateway, and plugins register in the
mcp_plugin_gateway.plugins entry-point group.
The plugin contract
A plugin is a Python distribution that declares exactly one entry point in the
mcp_plugin_gateway.plugins group. The entry point's value resolves to a
zero-argument callable that returns a single FastMCP instance.
# pyproject.toml of the plugin distribution
[project]
name = "my-plugin"
version = "0.1.0"
dependencies = ["mcp-plugin-gateway", "fastmcp>=4.1.4"]
[project.entry-points."mcp_plugin_gateway.plugins"]
my-plugin = "my_plugin:build_server"# src/my_plugin/__init__.py
from fastmcp import FastMCP
def build_server() -> FastMCP:
server = FastMCP("my-plugin")
@server.tool
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
return serverInstall the distribution (pip install -e .) and the plugin is discovered. The
callable is invoked lazily: importing the plugin's module and constructing its
server happen when a surface first needs that plugin's components, not at
discovery time. A plugin that needs configuration should therefore read it
inside build_server() or inside its own lifespan, not at import time.
Create a plugin
From the root of your project, scaffold a standalone plugin distribution:
mcp-plugin-gateway new-plugin my-pluginThe command creates mcp-gateway/my-plugin/ with its metadata, README, and starter
FastMCP server:
mcp-gateway/
└── my-plugin/
├── pyproject.toml
├── README.md
└── src/
└── my_plugin/
└── __init__.pyThe distribution name is the plugin's entry-point name; hyphens become
underscores in the Python package name. From the generated project directory,
install it into the same Python environment that runs the
mcp-plugin-gateway console script:
cd mcp-gateway/my-plugin
python -m pip install -e .Choose another project-relative destination with --directory:
mcp-plugin-gateway new-plugin my-plugin --directory plugins/my-pluginVerify that the plugin and its starter hello tool are discovered, then call
the tool. Its required name argument is a positional CLI argument:
mcp-plugin-gateway llm
mcp-plugin-gateway my-plugin hello AdaTo serve plugins for an MCP client, start the stdio server and configure the client to launch this command with its stdio transport:
mcp-plugin-gateway mcpAdd MCP to an existing Python project
If the current project has a PEP 621 pyproject.toml and exactly one
top-level Python package under src/ (or the project root), initialize it in
place:
mpg initThe command previews the changes to pyproject.toml and the new
<package>/mcp_server.py, then asks for confirmation. Declining makes no
changes. For a non-interactive run, review the printed plan and explicitly
approve it with:
mpg init --yesThis registers the existing distribution as a plugin and adds a starter
hello tool. It does not create a nested distribution or generate
project-specific tools. An agent can adapt the starter server to expose the
project's functionality. Unsupported or ambiguous layouts are rejected
without changing files.
Naming rules
The entry-point name is the plugin name. It is used as the namespace on the aggregate server (
my-plugin_add) and as the command group in the CLI.Names must be unique across installed distributions. Two distributions declaring the same name is an error that names both distributions; uninstall or rename one.
The name should be lowercase and hyphenated, matching the distribution name. FastMCP normalizes tool names, so avoid characters that cannot appear in a tool name.
The names of the built-in commands are reserved:
mcp,serve,llm,authoring,new-plugin, andconfig. A plugin that claims one of them is reported as a conflict and is not registered as a command group, so it cannot shadow the built-in command. Rename the plugin.
Contract rules
The callable must accept no arguments.
It must return a
FastMCPinstance. Returning anything else — or raising — is reported as a contract error naming the offending plugin and its distribution.The returned server should be bare: no transport is started and no authentication is configured. The system owns transport and mounting.
An empty
mcp_plugin_gateway.pluginsgroup is valid and yields no plugins. Entry points in other groups are ignored.
Surface tags
Tools are exposed on two surfaces: MCP clients and the generated CLI. Tags decide which surface sees a tool.
Tags on the tool | MCP clients | CLI |
(none) | visible | visible |
| visible | hidden |
| hidden | visible |
@server.tool
def shared(x: int) -> str:
"""Available on both surfaces."""
return str(x)
@server.tool(tags={"mcp"})
def remote_only() -> str:
"""MCP clients only."""
return "mcp"
@server.tool(tags={"cli"})
def local_only() -> str:
"""CLI only."""
return "cli"With the plugin above mounted as my-plugin, an MCP client lists my-plugin_shared
and my-plugin_remote_only, while the CLI offers my-plugin shared and
my-plugin local-only. Any other tag is rejected at registration with an error
naming the plugin and the tool, so a typo cannot silently change visibility.
mcp-plugin-gateway authoring prints this contract as a generated document, so an
agent reading it gets the current rules rather than a copy that can drift.
Related MCP server: basesmcp
Configuration
Configuration is per plugin and lazy. Each plugin gets its own settings class with an environment prefix derived from its name, so two plugins can use the same variable name without colliding. See the plugin's own documentation for the variables it reads.
Prefix convention
The prefix is the plugin name in upper case followed by an underscore, with -
replaced by _. Plugin alpha reads ALPHA_*; plugin my-plugin reads
MY_PLUGIN_*. A variable without a plugin's prefix is never read by that plugin.
# ~/.config/mcp_plugin_gateway/.env
ALPHA_API_KEY=alpha-key
BETA_API_KEY=beta-keyLocal development
Local runs read a single user-level file, ~/.config/mcp_plugin_gateway/.env. The
path is resolved through your home directory, so the file is genuinely read. A
.env shipped inside a plugin's own directory is ignored.
mkdir -p ~/.config/mcp_plugin_gateway
printf 'ALPHA_API_KEY=alpha-key\n' > ~/.config/mcp_plugin_gateway/.envDocker secrets
Set MCP_PLUGIN_GATEWAY_MODE=container to read settings from /run/secrets
instead of the user config file. In this mode the .env file is not read at all,
so secrets are authoritative. A missing /run/secrets directory only warns; the
remaining sources still apply.
Secret file names must carry the plugin prefix in upper case. A file named
ALPHA_TOKEN supplies plugin alpha's token setting; a file named
alpha_token is ignored. Docker secret names are conventionally lower case, so
declare the prefixed name explicitly:
services:
alpha:
environment:
MCP_PLUGIN_GATEWAY_MODE: container
secrets:
- source: alpha_token
target: ALPHA_TOKEN
secrets:
alpha_token:
file: ./secrets/alpha_token.txtPrecedence
Highest wins:
Process environment
.envfile (local mode only)Secrets directory (container mode only)
Declared defaults
Local and container modes never configure both file and secrets sources at once, because pydantic-settings silently prefers the file when both are present.
Build-time check
The container build verifies configuration so a missing variable fails the build rather than the first tool call. The check constructs every plugin's server without executing any tool:
MCP_PLUGIN_GATEWAY_MODE=container python -m mcp_plugin_gateway.settingsIt prints one line per plugin and exits non-zero if any plugin fails to build.
Container image and Compose
The repository ships a Dockerfile and a docker-compose.yml. The image runs the
build-time check above as a build step, so a missing secret fails docker build
instead of the first tool call. The secret is mounted as a BuildKit secret, so it
never lands in an image layer:
docker build \
--secret id=SECURE_PLUGIN_TOKEN,src=./secrets/SECURE_PLUGIN_TOKEN \
-t mcp-plugin-gateway:local .Building without that secret fails on purpose and names the missing variable:
docker build . # exits non-zero: "token ... Field required"docker-compose.yml serves each plugin as its own MCP service, so a plugin whose
settings are broken cannot take down the others. Every service runs the same image
with a different --plugin argument and reads its secret from /run/secrets:
docker compose config # validate
docker compose up -d # start one container per pluginThe secret file name must be the plugin-prefixed variable name in upper case
(SECURE_PLUGIN_TOKEN for plugin secure-plugin); a lowercase name is ignored.
See examples/README.md for the two example plugins and how to call them.
Usage
mcp-plugin-gateway --help # list discovered plugins
mcp-plugin-gateway my-plugin --help # list that plugin's CLI-visible tools
mcp-plugin-gateway my-plugin add 1 # call a tool; defaulted params are options
mcp-plugin-gateway my-plugin add 1 --b 2Commands are generated from each tool's input schema, so no CLI code is written by hand. The mapping is:
Schema | CLI |
required parameter | positional argument |
parameter with a default | named option ( |
enum | option validated against the choices |
array | repeatable flag ( |
boolean | paired flags ( |
nested object | single JSON option ( |
Underscores in parameter names become dashes: max_items is --max-items.
Every plugin's commands sit beneath a plugin-named group, even when the plugin
has a single tool. mcp-plugin-gateway alpha ping stays valid when alpha gains a
second tool.
A plugin whose settings are invalid does not break the CLI. It appears as a stub
group whose invocation reports the error, so --help still lists healthy
plugins.
Output format
Results print as human-readable text by default, matching what an MCP client sees. A tool that returns a string prints it verbatim, so markdown and tables reach the terminal unescaped; a scalar prints its bare value; a structured result prints as indented JSON.
Pass --json for the machine-readable envelope, either before the plugin name
or after the tool name:
mcp-plugin-gateway my-plugin greet Ada # Hello, Ada!
mcp-plugin-gateway my-plugin greet Ada --json # {"result": "Hello, Ada!"}
mcp-plugin-gateway --json my-plugin greet Ada # sameThe flag is consumed by the CLI and never forwarded to the tool.
Shell completion
mcp-plugin-gateway --install-completion bash # install for the current shell
mcp-plugin-gateway --show-completion bash # print the script insteadCompletion is dynamic: plugin names come from installed entry points and tool
names from the selected plugin's CLI-visible tools. A plugin's mcp-tagged tools
are not offered, because the CLI cannot call them.
Serving over MCP
mcp-plugin-gateway serve # serve all plugins over stdioContext for LLMs
Two commands print documents meant to be read by an agent rather than by a person. Both are generated at read time from live introspection, so neither can describe an install that no longer exists.
mcp-plugin-gateway llm # the installed plugins and their tools
mcp-plugin-gateway authoring # the contract a plugin must satisfyllm describes the install: every installed plugin, its tools, each tool's
parameters, and the surface each tool is reachable from. It is the authoritative
answer to "what can I call here?", and it is generated from the same
introspection the CLI and MCP surfaces are built from, so it cannot drift from
them.
The Markdown view keeps parameter summaries concise while showing enum choices,
nested object fields, array item types, and declared result shapes. The JSON
view is lossless: each tool's parameters field contains its complete input
schema, and output_schema contains its complete output schema or null when
none is declared.
# Installed plugins
1 plugin(s) installed.
## my-plugin
- Distribution: `my-plugin`
### add
Add two integers.
- Surfaces: cli, mcp
- Parameters:
- `a` (integer, required)
- `b` (integer, required)Narrow the document to one plugin with --plugin. An unknown name exits
non-zero and prints no document, so a caller never has to tell "absent" from
"broken":
mcp-plugin-gateway llm --plugin my-pluginA plugin that cannot be constructed is listed with its failure reason instead of being dropped, and the healthy plugins are still described. One broken plugin never hides the rest.
authoring describes the contract: the entry-point group, the required
callable shape, the surface tags, and a worked example. It is independent of
what is installed, so it stays complete in an environment with no plugins at
all. The example it shows is the same source the test suite installs and runs,
so it cannot drift from what actually works.
Both commands print markdown by default and a JSON document on request, with
--json accepted before or after the command name:
mcp-plugin-gateway llm --json # {"plugins": [...]}
mcp-plugin-gateway authoring --json # {"entry_point_group": "...", ...}Standalone plugin distributions
A plugin distribution can be the only thing installed in an environment. It
depends on mcp-plugin-gateway, declares its own mcp_plugin_gateway.plugins entry
point, and ships the mcp-plugin-gateway console script as a transitive
dependency. Users install one distribution and get both surfaces for its tools —
no host project, no extra wiring.
# pyproject.toml of the standalone plugin distribution
[project]
name = "my-plugin"
version = "0.1.0"
dependencies = ["mcp-plugin-gateway", "fastmcp"]
[project.entry-points."mcp_plugin_gateway.plugins"]
my-plugin = "my_plugin:build_server"pip install my-plugin
mcp-plugin-gateway --help # lists the my-plugin group
mcp-plugin-gateway add 1 2 # plugin name omitted: only one plugin installed
mcp-plugin-gateway mcp # serves my-plugin's tools without a prefixUnqualified versus namespaced tool names
The plugin name is optional when exactly one plugin is installed. The surfaces resolve the lone plugin and drop its namespace:
Installed plugins | CLI | MCP tool names |
one |
|
|
two or more |
|
|
With two or more plugins installed the name is required. An unnamed invocation exits non-zero and lists the installed plugin names instead of guessing:
Ambiguous plugin selection: 2 plugins are installed (my-plugin, other-plugin).
Name one explicitly, for example 'mcp-plugin-gateway my-plugin ...'.The explicit form always works, in both environments, so an invocation written for a multi-plugin environment keeps working when it is the only plugin:
mcp-plugin-gateway my-plugin add 1 2 # same result as the unnamed form
mcp-plugin-gateway mcp --plugin my-plugin # serve only this plugin, unqualifiedmcp-plugin-gateway mcp serves the aggregate server — namespaced tool names — only
when two or more plugins are installed. Naming a plugin with --plugin always
serves that plugin unqualified.
Entry points are read from the active interpreter's installed metadata. A plugin installed into a different virtual environment is invisible, so install the plugin into the environment that runs the console script.
Development
uv pip install -e . --python .venv/bin/python
uv pip install --group dev --python .venv/bin/python
.venv/bin/python -m pytestThe example plugins under examples/ are separate distributions. Install them to
see the surfaces in action; see examples/README.md.
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for progressive tool usage at any scale (see https://klavis.ai)
Host any HTTP API as a standard MCP server; auto-distribute to 12+ mainstream agent platforms.
Generate a typed SDK, CLI, and MCP server from any OpenAPI or GraphQL spec, and keep them current.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Related MCP Servers
- FlicenseBqualityDmaintenanceA Model Context Protocol server framework featuring dynamic tool loading and automatic tool discovery from a dedicated directory. It leverages FastMCP to provide a robust environment for building, configuring, and testing individual Python-based tools.1-
- AlicenseNot gradedqualityCmaintenanceEnables turning any Python function into an auto-registered MCP tool and serving them over a FastMCP server with auto-discovery, optional auth, and request context.MIT
- FlicenseNot gradedqualityAmaintenanceEnables LLMs to search for, add, configure, enable/disable, and aggregate tools from multiple MCP servers, dynamically extending their capabilities at runtime.-
- FlicenseNot gradedqualityAmaintenanceEnables LLMs to discover, add, configure, enable, disable, and aggregate other MCP servers, exposing their tools under unified prefixes and persisting configurations across sessions.-