Skip to main content

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 is mcp_server_toolkit for 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_changed notifications, 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

mcp_server_framework-1.5.2.tar.gz (48.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcp_server_framework-1.5.2-py3-none-any.whl (55.4 kB view details)

Uploaded Python 3

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

Hashes for mcp_server_framework-1.5.2.tar.gz
Algorithm Hash digest
SHA256 81ed8147b7acac443e1c092a64cd06ad4cbabd7871729d8bf721d38a00adc205
MD5 f436af048f56cd145a903496697a2ac7
BLAKE2b-256 61681bc2d5e7bb12c6853cc3d3f8195cfec0d76e2f9f46d6b83a9d179e252aa3

See more details on using hashes here.

File details

Details for the file mcp_server_framework-1.5.2-py3-none-any.whl.

File metadata

File hashes

Hashes for mcp_server_framework-1.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 50a4df03e5c067303f4d4978dd2e4e5c5ba3dfa070767334bde8cc9bd6bdd356
MD5 6bd72d471a8e96e0c1cf5a91fdf0219f
BLAKE2b-256 5ddb1be774a6f8bbcc51f6a8ceb10a196ec5b291aa5b44c68fbe415da7ac6f73

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page