MCP Server Toolkit — Framework, Factory and Proxy for building MCP servers
Project description
MCP Server Framework
A Python framework for building MCP (Model Context Protocol) servers. Implements MCP 1.26 core protocol (tools, resources, prompts, notifications). Optional features (completion, subscriptions, progress, cancellation) are not yet exposed through the plugin API.
PyPI:
pip install mcp-server-framework— repo name ismcp_server_toolkitfor historical reasons.
Three packages, one ecosystem:
| Package | Role |
|---|---|
| mcp_server_framework | Shared library: config, transport, health, plugin infrastructure |
| mcp_server_factory | Static plugin loading at startup (CLI tool) |
| mcp_server_proxy | Dynamic plugin loading at runtime (daemon with management API) |
Installation
pip install mcp-server-framework
For development:
pip install -e ".[dev]"
Factory — Build a static server from plugins
mcp-factory --plugins echo --http 12201 --plugin-dir ./plugins
Proxy — Dynamic server with runtime plugin management
# Start proxy with echo plugin on HTTP
mcp-proxy serve --autoload echo --http 12200 --plugin-dir ./plugins
# Manage plugins at runtime via CLI
mcp-proxy status
mcp-proxy load greet
mcp-proxy unload echo
mcp-proxy reload greet
Plugins can also be managed via MCP tools (proxy__load, proxy__unload, proxy__reload)
or the REST management API — from any connected client.
Framework — Build a standalone server
from mcp_server_framework import load_config, create_server, run_server
config = load_config()
mcp = create_server(config)
@mcp.tool()
def hello(name: str) -> str:
"""Says hello."""
return f"Hello, {name}!"
run_server(mcp, config)
Plugin Interface
Every plugin implements one function:
def register(mcp, config: dict) -> None:
@mcp.tool()
def my_tool(param: str) -> str:
"""Tool description for the LLM."""
return do_something(param)
@mcp.resource("myns://status")
def my_resource() -> str:
return "OK"
@mcp.prompt()
def my_prompt(text: str) -> str:
return f"Analyze: {text}"
Tools, resources, and prompts are all tracked and checked for collisions.
Works with Factory and Proxy without changes.
See plugins/demo_full.py for a complete example.
Plugin Loading
The framework supports two plugin sources:
Local plugins — files or directories in --plugin-dir:
mcp-proxy serve --autoload echo --plugin-dir ./plugins
Installed packages — dotted Python imports:
autoload:
- echo # local plugin from --plugin-dir
- mcp_shell_tools.shell # installed PyPI package
- mcp_wekan_tools.wekan # installed PyPI package
Dotted names are imported directly via importlib. Simple names go through the plugin directory search.
Create a New Plugin
./scripts/new-plugin.sh myservice # creates plugins/myservice/
./scripts/new-plugin.sh myservice ./src # custom target directory
Generates __init__.py (MCP wiring) and tools.py (pure logic) with annotated
examples — start editing tools.py, restart the proxy, done.
Recommended Plugin Structure
plugins/myservice/
├── __init__.py # register(mcp, config) — thin MCP wrapper
├── client.py # HTTP client (pure Python, no MCP)
└── tools.py # Tool logic (pure Python, no MCP)
This separation keeps your business logic testable and reusable without MCP dependencies.
Demo Plugins
| Plugin | Tools | Description |
|---|---|---|
echo |
2 | Minimal example — echo and echo_upper |
greet |
1 | Minimal example — greet by name |
demo_full |
1 | Reference: tool + resource + prompt registration |
Production tool plugins (shell, wekan, mattermost) are available as separate PyPI packages. See mcp_tools.
Proxy Features
Management API
The proxy runs a FastAPI management server on a separate port (default: 12299):
GET /proxy/status All plugins and tools
GET /proxy/plugins Plugin name list
POST /proxy/load {"plugin": "name"}
POST /proxy/unload {"plugin": "name"}
POST /proxy/reload {"plugin": "name"}
GET /proxy/commands Registered management extensions
POST /proxy/command/{name} Run a management extension
MCP Management Tools
The proxy registers management tools that any connected MCP client can call:
| Tool | Description |
|---|---|
proxy__load |
Load a plugin at runtime |
proxy__unload |
Unload a plugin |
proxy__reload |
Reload a plugin (picks up code changes) |
proxy__status |
Show loaded plugins, tools, resources, prompts |
proxy__list |
List available (not yet loaded) plugins |
proxy__tools |
List all loaded tools (supports dynamic_only filter) |
proxy__run |
Call a dynamically loaded tool by name (requires dynamic_dispatch: true) |
The server sends tools/list_changed, resources/list_changed, and prompts/list_changed
notifications when plugins are loaded or unloaded.
Management API Authentication
Optionally protect the management API with a Bearer token:
# proxy.yaml
management_token: "${MCP_MGMT_TOKEN}"
# Or via CLI
mcp-proxy serve --mgmt-token "my-secret" --autoload echo
# Client commands with token
mcp-proxy status --token "my-secret"
mcp-proxy load greet --token "my-secret"
# Or via environment variable (works for both server and client)
export MCP_MGMT_TOKEN="my-secret"
mcp-proxy serve --autoload echo
mcp-proxy status
Without a token configured, no authentication is required (development default).
Auto-Prefix
Avoid tool name collisions when loading multiple plugins:
# proxy.yaml
auto_prefix: true # tool "send" from plugin "mm" becomes "mm_send"
plugins:
my_plugin:
prefix: "mm" # custom prefix (overrides plugin name)
echo:
prefix: false # disable prefix for this plugin
Tools that already start with their plugin's prefix are not double-prefixed.
Health Endpoint
When running on HTTP, the health server includes plugin status:
GET /health Simple status
GET /health/detailed Uptime, requests, errors
GET /health/ready Readiness check (verifies plugins loaded)
GET /health/plugins Current plugin and tool inventory
Dynamic Dispatch
MCP clients cache the tool list and most don't handle tools/list_changed notifications.
This means tools loaded at runtime are invisible to the client. Dynamic dispatch solves this
with a single gateway tool proxy__run that can call any dynamically loaded tool by name.
# proxy.yaml
dynamic_dispatch: true # default: false
autoload:
- echo # static — visible in tools/list, callable directly
# At runtime: load a plugin dynamically
proxy__load greet → "Loaded 'greet': 1 tool. Call them via proxy__run(...)"
# Call dynamic tools via the gateway
proxy__run(tool="greet", arguments={"name": "Claude"})
# List only dynamic tools
proxy__tools(dynamic_only=true)
Security rule: Tools loaded at startup (autoload) are not callable via proxy__run —
they are visible in tools/list and the client can set permissions on them directly.
proxy__run only dispatches to tools loaded after startup.
Note: Dynamic dispatch is a workaround for the current MCP client ecosystem. Most clients (Claude.ai, VS Code, LibreChat) do not handle
tools/list_changednotifications, so runtime-loaded tools remain invisible to them. If clients start supporting dynamic tool discovery natively, this feature may be deprecated.
Management Command Extensions
Extend the proxy with custom management commands:
def register(mcp, config: dict) -> None:
proxy = config.get("_proxy")
if proxy:
proxy.register_command("stats", lambda p: f"{len(p.plugins)} plugins")
Commands are available via MCP tools and the REST management API.
OAuth Authentication
OAuth is enabled by default for HTTP transport (ignored for stdio).
Uses RFC 7662 Token Introspection — no extra dependencies needed (uses httpx from MCP SDK).
Valid tokens are cached for 8 hours (configurable via oauth_cache_ttl) to reduce introspection load.
Configure via YAML or environment variables:
# config.yaml
oauth_enabled: true # default: true
oauth_server_url: "https://auth.example.com" # OAuth introspection endpoint
oauth_public_url: "https://mcp.example.com" # Public URL of this server
oauth_cache_ttl: 28800 # token cache in seconds (default: 8h)
# Or via environment
export MCP_OAUTH_ENABLED=true
export MCP_OAUTH_SERVER_URL=https://auth.example.com
export MCP_PUBLIC_URL=https://mcp.example.com
Behavior:
- HTTP + OAuth configured → full token verification via introspection
- HTTP + OAuth enabled but URLs missing → warning, runs without auth
- HTTP +
oauth_enabled: false→ no auth, no warning - stdio → OAuth ignored regardless of config
To explicitly disable:
oauth_enabled: false
Plugin Registry (v1.4)
The framework provides PluginRegistry — a shared tracking layer used internally
by Factory and Proxy. If you build a custom server, you can use it directly:
from mcp_server_framework.plugins import PluginRegistry
registry = PluginRegistry(mcp, config)
result = registry.load_plugin("my_plugin")
if result.ok:
print(f"Loaded: {result.plugin.tools}")
Features:
- Collision detection for tools, resources, and prompts
load_plugin()/unload_plugin()with structured results (LoadResult,UnloadResult)get_summary(),all_tools,find_tool_owner()
Introspection Helpers
Pure functions for formatted output — no MCP dependency:
from mcp_server_framework.plugins import plugin_status, plugin_list, tool_list
print(plugin_status(registry)) # Server info + counts
print(plugin_list(registry)) # Plugin → tools table
print(tool_list(registry)) # Sorted tool list
Tool Call Loggers
Pluggable logging with four implementations:
from mcp_server_framework.plugins import (
JsonlToolLogger, # Daily JSONL + gzip archival + retention
TextToolLogger, # One-line text + size-based rotation
TranscriptLogger, # Markdown session transcripts
CompositeToolLogger, # Combines multiple loggers
)
logger = CompositeToolLogger([
JsonlToolLogger(log_dir),
TextToolLogger(log_file),
])
logger.log_call("echo", {"text": "hi"}, "hi", success=True)
Security
Pre-Call Validation
Register a custom validator that runs before every tool invocation:
from mcp_server_framework.plugins import set_pre_call_validator
def my_validator(tool_name: str, params: dict) -> str | None:
"""Return error string to reject, None to allow."""
if len(str(params)) > 100_000:
return "Input too large"
return None
set_pre_call_validator(my_validator)
Gate — Session-based TOTP Protection (v1.5)
mcp_server_framework.gate protects sensitive tool groups behind a TOTP second factor. All groups start locked on every proxy restart. A valid authenticator code unlocks a group for a configurable inactivity timeout (default: 2h). Any tool call resets the timer.
Quick start:
from mcp_server_framework.gate import Gate
def register(mcp, config):
gate = Gate(config.get("gate", {}))
gate.register_tools(mcp) # adds gate_unlock, gate_lock, gate_status
@mcp.tool()
@gate.protect("shell") # inner decorator — mcp.tool() must be outer
def shell_exec(command: str) -> str:
import subprocess
return subprocess.check_output(command, shell=True, text=True)
config.yaml:
gate:
enabled: true # false = tools work without authentication
secret_backend: env # env | file | vaultwarden
groups:
shell:
timeout: 7200 # inactivity timeout in seconds
secret_ref: MCP_GATE_SECRET_SHELL # env var name
vault:
timeout: 3600
secret_ref: MCP_GATE_SECRET_VAULT
MCP tools registered by gate.register_tools(mcp):
| Tool | Description |
|---|---|
gate_unlock(group, code) |
Unlock a group with 6-digit TOTP code |
gate_lock(group="") |
Lock a group, or all groups — kill switch |
gate_status() |
Show status and remaining time per group |
Secret backends:
| Backend | Config | Dependencies | Rotation |
|---|---|---|---|
env |
secret_ref: ENV_VAR_NAME |
none | restart |
file |
secret_ref: key, secret_file: ~/.mcp_gate_secrets |
none | immediate |
vaultwarden |
secret_ref: item-name, vaultwarden_url/token |
requests |
after TTL |
Security properties: lockout after 5 consecutive wrong codes, no persistent unlock state across restarts, kill switch via gate_lock().
See gate/README.md for full documentation.
Logging
Configure log format via config or environment:
log_level: INFO
log_format: json # "json" for machine-readable, "text" (default) for humans
JSON output example:
{"ts": "2026-03-11T10:30:00+00:00", "level": "INFO", "logger": "mcp_server_proxy.proxy", "msg": "Plugin 'echo' loaded: 2 tools ['echo', 'echo_upper']"}
Tool Call Logging (Proxy)
The proxy uses JsonlToolLogger from the framework to log every tool call to ~/.mcp_proxy/logs/tool_calls.jsonl:
{"ts": "2026-03-11T14:30:00", "tool": "echo", "params": {"text": "hello"}, "result": "hello", "ok": true}
- Daily rotation with gzip compression (
YYYY-MM-DD.jsonl.gz) - 90-day retention with automatic cleanup
- Safe — never raises, never blocks tool execution
Interactive Test Client
Debug and explore any MCP server from the terminal:
# Connect via stdio
python examples/mcp_client.py -v stdio -- mcp-proxy --autoload echo
# Connect via HTTP
python examples/mcp_client.py http http://localhost:12200/mcp
REPL commands: tools, call <name>, resources, prompts, info, quit
Tool shorthand — call tools directly by name:
mcp> echo hello # single-arg shorthand
mcp> echo_upper world
mcp> greet Claude
mcp> proxy__status # no-arg tools work too
mcp> call echo # interactive mode (prompts for args)
Examples
Ready-to-run scripts in examples/:
./examples/run_factory_echo.sh # Factory + Echo (stdio, all-in-one)
./examples/run_proxy_http.sh # Proxy + Echo (HTTP)
./examples/run_proxy_full.sh # Proxy + Echo + Greet (HTTP)
./examples/connect_proxy_http.sh # Client → running proxy
See examples/README.md for details.
Configuration
YAML config with environment variable overrides (MCP_*):
server_name: "My Proxy"
transport: http # stdio | http
host: "0.0.0.0"
port: 12200
health_port: 12201
management_port: 12299
log_level: INFO
log_format: text # text | json
auto_prefix: true
dynamic_dispatch: false # true = enable proxy__run for runtime-loaded tools
# OAuth (enabled by default for HTTP, set false to disable)
oauth_server_url: "https://auth.example.com"
oauth_public_url: "https://mcp.example.com"
oauth_cache_ttl: 28800 # token cache seconds (default: 8h, 0 = disabled)
# Management API auth (separate from OAuth, optional)
management_token: "${MCP_MGMT_TOKEN}"
autoload:
- echo # local plugin
- mcp_shell_tools.shell # installed PyPI package
plugins:
echo:
enabled: true
Plugin-Specific Configuration
Credentials and plugin-specific settings go into a separate config file per plugin, not into the proxy config:
~/mcp_plugins/{plugin_name}/config.yaml
The proxy loads these automatically. If no plugin config file exists, it falls
back to the plugins: section in the proxy config.
Example configs in config/:
| Config | Use case |
|---|---|
proxy.example.yaml |
HTTP proxy with all options documented |
config-stdio.yaml |
stdio for Claude Code / Claude Desktop |
framework.example.yaml |
Standalone framework server |
factory.example.yaml |
Factory with static plugins |
Claude Code / Claude Desktop
Use the proxy as a stdio MCP server — no OAuth, no ports:
{
"mcpServers": {
"proxy": {
"command": "/path/to/mcp_server_toolkit/.venv/bin/mcp-proxy",
"args": ["serve", "--config", "config/config-stdio.yaml",
"--plugin-dir", "./plugins"]
}
}
}
Tests
pytest # all tests
pytest -v # verbose
pytest tests/proxy/ # proxy only
Project Structure
src/
├── mcp_server_framework/ # Shared: config, server, health, plugins
│ └── plugins/ # PluginRegistry, ToolLogger, Introspection
├── mcp_server_factory/ # Static loader + CLI
└── mcp_server_proxy/ # Dynamic loader + management API + CLI
plugins/
├── echo.py # Minimal example
├── greet.py # Minimal example
└── demo_full.py # Reference: tool + resource + prompt
examples/
├── mcp_client.py # Interactive test client
├── configs/ # Ready-to-use YAML configs
└── *.sh # Launch scripts
scripts/
└── new-plugin.sh # Plugin scaffold generator
tests/ # Tests (framework, factory, proxy)
config/ # Example configs + systemd service
Production tool plugins are maintained in a separate repository: mcp_tools — shell, wekan, mattermost, and more.
License
MIT
Project details
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_server_framework-1.5.2.tar.gz.
File metadata
- Download URL: mcp_server_framework-1.5.2.tar.gz
- Upload date:
- Size: 48.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81ed8147b7acac443e1c092a64cd06ad4cbabd7871729d8bf721d38a00adc205
|
|
| MD5 |
f436af048f56cd145a903496697a2ac7
|
|
| BLAKE2b-256 |
61681bc2d5e7bb12c6853cc3d3f8195cfec0d76e2f9f46d6b83a9d179e252aa3
|
File details
Details for the file mcp_server_framework-1.5.2-py3-none-any.whl.
File metadata
- Download URL: mcp_server_framework-1.5.2-py3-none-any.whl
- Upload date:
- Size: 55.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
50a4df03e5c067303f4d4978dd2e4e5c5ba3dfa070767334bde8cc9bd6bdd356
|
|
| MD5 |
6bd72d471a8e96e0c1cf5a91fdf0219f
|
|
| BLAKE2b-256 |
5ddb1be774a6f8bbcc51f6a8ceb10a196ec5b291aa5b44c68fbe415da7ac6f73
|