MCP Proxy Adapter
Author: Vasiliy Zdanovskiy
Email: vasilyvz@gmail.com
Cursor / agent rules: the repository overlay is docs/project_overlay.md; the operating contract lives at the repo root — CLAUDE.md with its claude/ package. Agent role behaviour is defined by that contract, not by these docs.
Automated tests live under tests/ (pytest). Manual / integration harnesses, example runners, and release helpers are under scripts/ (for example scripts/test_all_examples.py). The full release-readiness pipeline is python -m pipeline.
Overview
MCP Proxy Adapter is a comprehensive framework for building JSON-RPC API servers with built-in security, SSL/TLS support, and proxy registration capabilities. It provides a unified interface for command execution, protocol management, and security enforcement.
Features
- JSON-RPC API: Full JSON-RPC 2.0 support with built-in commands
- Security Framework: Integrated authentication, authorization, and SSL/TLS
- Protocol Management: HTTP, HTTPS, and mTLS protocol support
- Proxy Registration: Automatic registration with proxy servers
- Command System: Extensible command registry with built-in commands
- Configuration Management: Comprehensive configuration with environment variable overrides
- Queue-Backed Commands: Fire-and-forget execution for long-running operations (NLP pipelines, ML inference, etc.)
- HTTP layer returns
job_idimmediately without blocking - Heavy processing runs in separate worker processes
- Client polls job status independently without HTTP timeout constraints
- Supports operations that take minutes or hours to complete
- HTTP layer returns
Quick Start
-
Installation:
pip install mcp-proxy-adapter
-
Generate Configuration:
# Generate a simple HTTP configuration adapter-cfg-gen --protocol http --out config.json # Generate HTTPS configuration with proxy registration adapter-cfg-gen --protocol https --with-proxy --out config.json # Generate mTLS configuration with custom certificates adapter-cfg-gen --protocol mtls \ --server-cert-file ./certs/server.crt \ --server-key-file ./certs/server.key \ --server-ca-cert-file ./certs/ca.crt \ --out config.json
-
Validate Configuration:
# Validate configuration file adapter-cfg-val --file config.json
-
Start Server:
# Use the generated configuration python -m mcp_proxy_adapter --config config.json # Or use the main CLI mcp-proxy-adapter config validate --file config.json mcp-proxy-adapter server --config config.json
-
Access the API:
- Health check:
GET http://localhost:8000/health - JSON-RPC:
POST http://localhost:8000/api/jsonrpc - Documentation:
http://localhost:8000/docs
GET /healthand thehealthJSON-RPC command report a top-levelstatusof"ok"only when there is no genuine queue-liveness evidence against it. Whenqueue_manageris enabled (the default),components.queuecarries the live queue liveness payload ({"enabled": true, "status": "healthy" | "degraded" | "unhealthy", ...}, the same dataqueue_healthreturns); a dead queue (e.g. jobs stuck pending with zero running workers, often EMFILE) flips the top-levelstatusto"degraded". An advisory-only issue (e.g. a lowrlimit_nofile) can still mark the component's ownstatusas"degraded"without moving the top-level status - seederive_overall_status()inmcp_proxy_adapter/integrations/queuemgr_integration/_health.pyfor the exact liveness-vs-advisory classification. Whenqueue_manageris disabled,components.queueis{"enabled": false}and the top-level status stays"ok".GET /healthalways returns HTTP 200 when the server answered at all - the degradation is conveyed by the body, never by the HTTP status code, so this package's own clients (which treat a non-2xx/healthresponse as "server unreachable") keep seeing a degraded-but-reachable server correctly. - Health check:
Queue-Backed Commands (Fire-and-Forget Execution)
Starting from version 6.9.96+, mcp-proxy-adapter supports fire-and-forget execution for queue-backed commands. This enables reliable handling of long-running operations (NLP pipelines, ML inference, data processing, etc.) without HTTP timeout constraints.
How It Works
- Client submits command via JSON-RPC or REST API
- Server enqueues job and returns
job_idimmediately (typically < 1 second) - Heavy processing runs inside a separate queue worker process
- Client polls job status independently using
queue_get_job_status(job_id) - No HTTP timeout issues - the initial request completes quickly, and polling uses separate requests
Sync-Cap Timeout Fallback
A synchronous (use_queue = False, the default) command that runs past the
configured sync cap (queue_manager.command_timeout, 20s by default) is never
cancelled - it keeps running to completion in the background - and the request
falls back to the same job_id/poll contract as a queue-backed command:
{"success": true, "job_id": "...", "status": "pending",
"message": "Command '<name>' exceeded sync timeout, track via queue_get_job_status",
"store": "queuemgr", "poll_with": "queue_get_job_status", "queued_after_timeout": true}
store names the command family that serves this job_id, not a literal
storage location: every queue_* command (queue_get_job_status,
queue_list_jobs, queue_stop_job, queue_delete_job) works on a
sync-cap-timeout job_id exactly as it does on a real queue-backed job's
job_id - queue_get_job_status publishes the ORIGINAL execution's own
result once it finishes (never a duplicate re-execution), queue_list_jobs
lists it as a normal row, and queue_stop_job/queue_delete_job work on it
too. A client never needs to branch on store; poll_with always names the
one command to poll (queue_get_job_status).
queue_stop_job on a sync-cap job_id interrupts nothing. The execution
is already running inside the server process and is deliberately never
cancelled: a command that offloads its work to a worker thread would ignore
the cancellation anyway, and cancelling the awaitable while that thread runs
on is what corrupts request handling process-wide. So a stop request on a
running sync-cap job_id is answered honestly rather than obeyed - the
command still returns success: true, and its payload reports what actually
happened:
{"job_id": "...", "status": "running", "stop_requested": true,
"requested_status": "stopped", "actual_status": "running", "stopped": false,
"description": "Stop requested; this job is a sync-cap detached execution running in-process and cannot be interrupted - it will run to completion and publish its own terminal result (poll queue_get_job_status)."}
stop_requested is recorded on the fallback job's own manager-level metadata
and appears in this queue_stop_job reply only. It is not visible again
afterwards: queue_get_job_status never serializes metadata into its
response, so a later status poll cannot show that a stop was previously
requested.
Keep polling queue_get_job_status: the execution runs to completion and
publishes its own terminal result under the same job_id. Stopping an
already-finished sync-cap job_id is a no-op that idempotently reports that
job's terminal status with success: true - unlike a real queue-backed job,
whose queue_stop_job on an already-terminal job_id returns a JSON-RPC
error (-32603). Use queue_delete_job if you simply want to stop tracking
the id. A queue-backed command
(use_queue = True) is unaffected: it runs in its own queuemgr worker process
and queue_stop_job stops it for real.
Client Usage Example
import asyncio
from mcp_proxy_adapter.client.jsonrpc_client.client import JsonRpcClient
async def main():
# For mTLS or slow networks, increase HTTP timeout
client = JsonRpcClient(
protocol="mtls",
host="127.0.0.1",
port=8080,
cert="/path/to/cert.crt",
key="/path/to/key.key",
ca="/path/to/ca.crt",
timeout=60.0, # HTTP client timeout: 60 seconds (default: 30.0)
)
try:
# Execute queue-backed command with automatic polling
result = await client.execute_command_unified(
command="chunk", # Your long-running command
params={"text": "Very long text...", "window": 3},
auto_poll=True, # Automatically poll until completion
poll_interval=1.0, # Check status every 1 second
timeout=600.0, # Overall timeout: 10 minutes
)
print(f"Job completed: {result['status']}")
print(f"Result: {result['result']}")
except TimeoutError:
print("Job did not complete within timeout")
finally:
await client.close()
asyncio.run(main())
Note: The timeout parameter in JsonRpcClient controls the HTTP client timeout for all requests (including status polling). For mTLS connections or slow networks, you may need to increase this value. You can also set it via the MCP_PROXY_ADAPTER_HTTP_TIMEOUT environment variable.
HTTP Timeout Configuration
The HTTP client timeout can be configured in three ways (in order of precedence):
-
Constructor parameter (highest priority):
client = JsonRpcClient(timeout=60.0) # 60 seconds
-
Environment variable:
export MCP_PROXY_ADAPTER_HTTP_TIMEOUT=60.0
-
Default value: 30.0 seconds (if neither parameter nor environment variable is set)
This timeout applies to all HTTP requests including:
- Command execution
- Status polling (
queue_get_job_status) - Health checks
- Proxy registration
For mTLS connections or slow networks, consider increasing the timeout to avoid httpx.ReadTimeout errors.
Server-Side Command Definition
To enable queue execution for a command, set use_queue = True:
from mcp_proxy_adapter.commands.base import Command
from mcp_proxy_adapter.commands.result import SuccessResult
class ChunkCommand(Command):
"""Example: Long-running NLP chunking command."""
name = "chunk"
descr = "Chunk long text into smaller pieces"
use_queue = True # CRITICAL: Enable queue execution
@classmethod
def get_schema(cls):
return {
"type": "object",
"properties": {
"text": {"type": "string"},
"window": {"type": "integer", "default": 3},
},
"required": ["text"],
}
async def execute(self, text: str, window: int = 3, **kwargs):
# Heavy processing happens here
# This runs in a separate process, not in HTTP handler
chunks = process_text(text, window)
return SuccessResult(data={"chunks": chunks})
Advanced Features
- Manual Polling: Get
job_idimmediately and poll status manually - Progress Hooks: Receive progress updates via callback functions
- Parallel Execution: Submit multiple commands and poll them concurrently
- Error Handling: Proper handling of job failures, timeouts, and network errors
For detailed examples, see:
mcp_proxy_adapter/examples/queue_fire_and_forget_example.py
Requirements
mcp-proxy-adapter >= 6.9.96queuemgr >= 1.0.14(completed jobs retention; manager control-path timeouts fix for add_job / get_job_status / stop_job)
Nuances
Configurable public paths (security)
UnifiedSecurityMiddleware treats some paths as public (no API key required). By default: /health, /docs, /openapi.json. You can add more via config so that e.g. a WebSocket endpoint is reachable without auth on the HTTP upgrade:
{
"security": {
"enabled": true,
"tokens": { "admin": "your-secret-key" },
"public_paths": ["/health", "/docs", "/openapi.json", "/ws"]
}
}
Alternatively use security.auth.public_paths. Transfer chunk routes (/api/transfer/*) use the same UnifiedSecurityMiddleware and headers (e.g. X-API-Key, mTLS) as JSON-RPC — not a separate anonymous data channel when security is enabled.
Job status and job_id
Commands like embed_job_status and queue_get_job_status require a job_id. The adapter accepts it in params or at the request top level (for clients that send job_id outside params). If job_id is missing, the adapter returns a clear error instead of forwarding an invalid request.
- Prefer:
{"method": "embed_job_status", "params": {"job_id": "<uuid>"}} - Also accepted:
{"method": "embed_job_status", "params": {}, "job_id": "<uuid>"}
WebSocket client (push instead of polling)
The client can wait for job completion via a WebSocket channel (/ws) instead of polling:
- One-shot:
await client.wait_for_job_via_websocket(job_id, timeout=60.0)— connect, subscribe, return on first terminal event (job_completed/job_failed). - Bidirectional channel:
open_bidirectional_ws_channel(client)(orclient.open_bidirectional_ws_channel()) — useasync with channel, thensend_json(...)to send (e.g. subscribe/unsubscribe) andasync for msg in channel.receive_iter(): ...to consume server events.
Same host, port, auth (X-API-Key or Bearer), and TLS as the JSON-RPC client. Ensure the server exposes /ws and includes /ws in security.public_paths so the upgrade is not blocked.
Subscribe/unsubscribe messages follow the same JSON patterns as job WebSocket delivery (see server job_push handlers and client open_bidirectional_ws_channel).
Running WebSocket client examples locally
To try the WebSocket client examples without a real embed service, start the minimal test server (provides embed_queue and /ws):
python examples/run_ws_test_server.py --port 8090
Then in another terminal:
MCP_PORT=8090 python examples/client_websocket_job_status.py
MCP_PORT=8090 python examples/client_bidirectional_websocket.py
Configuration
The adapter uses a comprehensive JSON configuration file (config.json) that includes all available options. All features are disabled by default and must be explicitly enabled. The configuration system has NO default values - all configuration must be explicitly specified.
Configuration Sections
1. uuid (Root Level)
Type: string (UUID4 format)
Required: YES
Description: Unique identifier for the server instance
Format: xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx
{
"uuid": "123e4567-e89b-42d3-a456-426614174000"
}
2. server Section
Required: YES
Description: Core server configuration settings
| Field | Type | Required | Description | Allowed Values |
|---|---|---|---|---|
host |
string | YES | Server host address | Any valid IP or hostname |
port |
integer | YES | Server port number | 1-65535 |
protocol |
string | YES | Server protocol | "http", "https", "mtls" |
debug |
boolean | YES | Enable debug mode | true, false |
log_level |
string | YES | Logging level | "DEBUG", "INFO", "WARNING", "ERROR" |
{
"server": {
"host": "0.0.0.0",
"port": 8080,
"protocol": "http",
"debug": false,
"log_level": "INFO"
}
}
3. logging Section
Required: YES
Description: Logging configuration settings
| Field | Type | Required | Description |
|---|---|---|---|
level |
string | YES | Log level ("INFO", "DEBUG", "WARNING", "ERROR") |
log_dir |
string | YES | Directory for log files |
log_file |
string | YES | Main log file name |
error_log_file |
string | YES | Error log file name |
access_log_file |
string | YES | Access log file name |
max_file_size |
string/integer | YES | Maximum log file size ("10MB" or 10485760) |
backup_count |
integer | YES | Number of backup log files |
format |
string | YES | Log message format (Python logging format string) |
date_format |
string | YES | Date format for logs |
console_output |
boolean | YES | Enable console logging |
file_output |
boolean | YES | Enable file logging |
{
"logging": {
"level": "INFO",
"log_dir": "./logs",
"log_file": "mcp_proxy_adapter.log",
"error_log_file": "mcp_proxy_adapter_error.log",
"access_log_file": "mcp_proxy_adapter_access.log",
"max_file_size": "10MB",
"backup_count": 5,
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"date_format": "%Y-%m-%d %H:%M:%S",
"console_output": true,
"file_output": true
}
}
4. commands Section
Required: YES
Description: Command management configuration
| Field | Type | Required | Description |
|---|---|---|---|
auto_discovery |
boolean | YES | Enable automatic command discovery |
commands_directory |
string | YES | Directory for command files |
enabled_commands |
array | YES | List of enabled commands |
disabled_commands |
array | YES | List of disabled commands |
custom_commands_path |
string | YES | Path to custom commands |
{
"commands": {
"auto_discovery": true,
"commands_directory": "./commands",
"enabled_commands": ["health", "echo", "help"],
"disabled_commands": [],
"custom_commands_path": "./commands"
}
}
5. transport Section
Required: YES
Description: Transport layer configuration
| Field | Type | Required | Description | Allowed Values |
|---|---|---|---|---|
type |
string | YES | Transport type | "http", "https", "mtls" |
port |
integer/null | YES | Transport port (can be null) | 1-65535 or null |
verify_client |
boolean | YES | Enable client certificate verification | true, false |
chk_hostname |
boolean | YES | Enable hostname checking | true, false |
Nested Section: transport.ssl (when SSL/TLS is enabled)
| Field | Type | Required | Description |
|---|---|---|---|
enabled |
boolean | Conditional | Enable SSL/TLS |
cert_file |
string | Conditional | Path to SSL certificate file |
key_file |
string | Conditional | Path to SSL private key file |
ca_cert |
string | Optional | Path to CA certificate file |
verify_client |
boolean | Optional | Verify client certificates |
verify_ssl |
boolean | Optional | Verify SSL certificates |
check_hostname |
boolean | Optional | Verify hostname in certificate (alias: dnscheck) |
verify_mode |
string | Optional | SSL verification mode: "CERT_NONE", "CERT_OPTIONAL", "CERT_REQUIRED" |
{
"transport": {
"type": "https",
"port": 8443,
"verify_client": false,
"chk_hostname": true,
"ssl": {
"enabled": true,
"cert_file": "./certs/server.crt",
"key_file": "./certs/server.key",
"ca_cert": "./certs/ca.crt",
"verify_ssl": true,
"check_hostname": true,
"verify_mode": "CERT_REQUIRED"
}
}
}
6. ssl Section (Root Level)
Required: Conditional (required for HTTPS/mTLS protocols)
Description: SSL/TLS configuration for server
| Field | Type | Required | Description |
|---|---|---|---|
enabled |
boolean | YES | Enable SSL/TLS |
cert_file |
string | YES | Path to SSL certificate file |
key_file |
string | YES | Path to SSL private key file |
ca_cert |
string | Optional | Path to CA certificate file (required for mTLS) |
{
"ssl": {
"enabled": true,
"cert_file": "./certs/server.crt",
"key_file": "./certs/server.key",
"ca_cert": "./certs/ca.crt"
}
}
7. registration Section
Required: YES
Description: Proxy server registration configuration. This is the
section name and shape SimpleConfig.load() and the live registration
resolver (api/core/registration/) actually read - the legacy
proxy_registration name is read by neither and is silently ignored as an
unrecognized top-level section (bug fd45567e).
| Field | Type | Required | Description |
|---|---|---|---|
enabled |
boolean | YES | Enable proxy registration |
register_url |
string | YES (when enabled) | Full registration endpoint URL, e.g. "https://proxy.example.com:3005/register" |
unregister_url |
string | Optional | Full unregistration endpoint URL |
server_id |
string | YES | Unique server identifier |
server_name |
string | Optional | Human-readable server name |
instance_uuid |
string | YES (when enabled) | Strict UUID4 identifying this server instance. Startup validation rejects the configuration without it (registration.instance_uuid is required when registration.enabled=true), so the top-level uuid is not a usable fallback here — the server never reaches the code that would read it |
protocol |
string | Conditional | Registration protocol: "http", "https", "mtls" |
auto_on_startup |
boolean | Optional (default true) |
Auto-register on startup |
auto_on_shutdown |
boolean | Optional (default true) |
Auto-unregister on shutdown |
Nested Section: registration.ssl (when using HTTPS/mTLS)
| Field | Type | Required | Description |
|---|---|---|---|
cert |
string | Conditional | Path to client certificate (for mTLS) |
key |
string | Conditional | Path to client key (for mTLS) |
ca |
string | Conditional | Path to CA certificate |
crl |
string | Optional | Path to certificate revocation list |
check_hostname |
boolean | Conditional | Verify proxy hostname (alias: dnscheck) |
Nested Section: registration.heartbeat
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | YES (when enabled) | Full heartbeat endpoint URL, e.g. "https://proxy.example.com:3005/proxy/heartbeat" |
interval |
integer | Optional (default 30) |
Heartbeat interval in seconds |
{
"registration": {
"enabled": true,
"protocol": "mtls",
"register_url": "https://proxy.example.com:3005/register",
"unregister_url": "https://proxy.example.com:3005/unregister",
"server_id": "my-server-001",
"server_name": "My MCP Server",
"instance_uuid": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"auto_on_startup": true,
"auto_on_shutdown": true,
"ssl": {
"cert": "./certs/client.crt",
"key": "./certs/client.key",
"ca": "./certs/ca.crt",
"crl": null,
"check_hostname": false
},
"heartbeat": {
"url": "https://proxy.example.com:3005/proxy/heartbeat",
"interval": 30
}
}
}
8. security Section
Required: YES
Description: Security framework configuration
| Field | Type | Required | Description |
|---|---|---|---|
enabled |
boolean | YES | Enable security framework |
tokens |
object | YES | Token-based authentication configuration |
roles |
object | YES | Role-based access control configuration |
roles_file |
string/null | YES | Path to roles configuration file |
Nested Section: security.tokens
| Field | Type | Description |
|---|---|---|
admin |
string | Administrator token |
user |
string | User token |
readonly |
string | Read-only token |
| (custom) | string | Custom token names |
Nested Section: security.roles
| Field | Type | Description |
|---|---|---|
admin |
array | Administrator role permissions |
user |
array | User role permissions |
readonly |
array | Read-only role permissions |
| (custom) | array | Custom role names |
{
"security": {
"enabled": true,
"tokens": {
"admin": "admin-secret-key",
"user": "user-secret-key",
"readonly": "readonly-secret-key"
},
"roles": {
"admin": ["*"],
"user": ["health", "echo"],
"readonly": ["health"]
},
"roles_file": null
}
}
9. roles Section
Required: YES
Description: Role-based access control configuration
| Field | Type | Required | Description |
|---|---|---|---|
enabled |
boolean | YES | Enable RBAC |
config_file |
string/null | YES | Path to roles configuration file |
default_policy |
object | YES | Default policy settings |
auto_load |
boolean | YES | Auto-load roles on startup |
validation_enabled |
boolean | YES | Enable role validation |
Nested Section: roles.default_policy
| Field | Type | Description |
|---|---|---|
deny_by_default |
boolean | Deny access by default |
require_role_match |
boolean | Require exact role match |
case_sensitive |
boolean | Case-sensitive role matching |
allow_wildcard |
boolean | Allow wildcard permissions |
{
"roles": {
"enabled": false,
"config_file": null,
"default_policy": {
"deny_by_default": true,
"require_role_match": true,
"case_sensitive": false,
"allow_wildcard": true
},
"auto_load": true,
"validation_enabled": true
}
}
10. debug Section
Required: YES
Description: Debug mode configuration
| Field | Type | Required | Description | Allowed Values |
|---|---|---|---|---|
enabled |
boolean | YES | Enable debug mode | true, false |
level |
string | YES | Debug level | "DEBUG", "INFO", "WARNING", "ERROR" |
{
"debug": {
"enabled": false,
"level": "WARNING"
}
}
Protocol-Specific Requirements
HTTP Protocol
Required Sections: server, logging, commands, transport, debug, security, roles
SSL Required: NO
Client Verification: NO
HTTPS Protocol
Required Sections: All sections + ssl
SSL Required: YES
Client Verification: NO
Required Files:
ssl.cert_file- Server certificatessl.key_file- Server private key
mTLS Protocol
Required Sections: All sections + ssl
SSL Required: YES
Client Verification: YES
Required Files:
ssl.cert_file- Server certificatessl.key_file- Server private keyssl.ca_cert- CA certificate for client verification
Configuration Validation
The framework automatically validates configuration on load:
- Required sections: All mandatory configuration sections are present
- Required keys: All required keys within sections are present
- Type validation: All values have correct data types
- File existence: All referenced files exist (when features are enabled)
- Feature dependencies: All feature dependencies are satisfied
- UUID format: UUID4 format validation
- Certificate validation: Certificate format, expiration, key matching
Complete Configuration Example
{
"uuid": "123e4567-e89b-42d3-a456-426614174000",
"server": {
"host": "0.0.0.0",
"port": 8080,
"protocol": "mtls",
"debug": false,
"log_level": "INFO"
},
"logging": {
"level": "INFO",
"log_dir": "./logs",
"log_file": "mcp_proxy_adapter.log",
"error_log_file": "mcp_proxy_adapter_error.log",
"access_log_file": "mcp_proxy_adapter_access.log",
"max_file_size": "10MB",
"backup_count": 5,
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"date_format": "%Y-%m-%d %H:%M:%S",
"console_output": true,
"file_output": true
},
"commands": {
"auto_discovery": true,
"commands_directory": "./commands",
"enabled_commands": ["health", "echo", "help"],
"disabled_commands": [],
"custom_commands_path": "./commands"
},
"transport": {
"type": "mtls",
"port": 8443,
"verify_client": true,
"chk_hostname": true,
"ssl": {
"enabled": true,
"cert_file": "./certs/server.crt",
"key_file": "./certs/server.key",
"ca_cert": "./certs/ca.crt",
"verify_ssl": true,
"check_hostname": true,
"verify_mode": "CERT_REQUIRED"
}
},
"ssl": {
"enabled": true,
"cert_file": "./certs/server.crt",
"key_file": "./certs/server.key",
"ca_cert": "./certs/ca.crt"
},
"registration": {
"enabled": true,
"protocol": "mtls",
"register_url": "https://proxy.example.com:3005/register",
"unregister_url": "https://proxy.example.com:3005/unregister",
"server_id": "my-server-001",
"server_name": "My MCP Server",
"instance_uuid": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"auto_on_startup": true,
"auto_on_shutdown": true,
"ssl": {
"cert": "./certs/client.crt",
"key": "./certs/client.key",
"ca": "./certs/ca.crt",
"crl": null,
"check_hostname": false
},
"heartbeat": {
"url": "https://proxy.example.com:3005/proxy/heartbeat",
"interval": 30
}
},
"debug": {
"enabled": false,
"level": "WARNING"
},
"security": {
"enabled": true,
"tokens": {
"admin": "admin-secret-key",
"user": "user-secret-key",
"readonly": "readonly-secret-key"
},
"roles": {
"admin": ["*"],
"user": ["health", "echo"],
"readonly": ["health"]
},
"roles_file": null
},
"roles": {
"enabled": false,
"config_file": null,
"default_policy": {
"deny_by_default": true,
"require_role_match": true,
"case_sensitive": false,
"allow_wildcard": true
},
"auto_load": true,
"validation_enabled": true
}
}
For more detailed configuration documentation, see docs/ALL_CONFIG_SETTINGS.md.
SimpleConfig Format
The framework supports a simplified configuration format (SimpleConfig) that provides a minimal, explicit configuration model with three main sections: server, client, and registration. Each section can operate independently with its own protocol (HTTP, HTTPS, or mTLS), certificates, keys, and CRL (Certificate Revocation List).
SimpleConfig Structure
{
"server": { ... },
"client": { ... },
"registration": { ... },
"auth": { ... }
}
1. server Section
Purpose: Server endpoint configuration (listening for incoming connections)
| Field | Type | Required | Description | Allowed Values |
|---|---|---|---|---|
host |
string | YES | Server host address | Any valid IP or hostname |
port |
integer | YES | Server port number | 1-65535 |
protocol |
string | YES | Server protocol | "http", "https", "mtls" |
cert_file |
string | Conditional | Server certificate file path | Valid file path (required for HTTPS/mTLS) |
key_file |
string | Conditional | Server private key file path | Valid file path (required for HTTPS/mTLS) |
ca_cert_file |
string | Conditional | CA certificate file path | Valid file path (required for mTLS if use_system_ca=false) |
crl_file |
string | Optional | Certificate Revocation List file path | Valid CRL file path |
use_system_ca |
boolean | NO | Allow system CA store when ca_cert_file is not provided |
true, false (default: false) |
log_dir |
string | NO | Directory for log files | Valid directory path (default: "./logs") |
Protocol Requirements:
- HTTP: No certificates required
- HTTPS:
cert_fileandkey_fileare optional but recommended. If one is specified, both must be provided. - mTLS:
cert_fileandkey_fileare required.ca_cert_fileis required ifuse_system_ca=false(default).
CRL Validation:
- If
crl_fileis specified, it must:- Exist and be accessible
- Be a valid CRL file format (PEM or DER)
- Not be expired (checked against
next_updatefield) - Pass format validation
- If CRL validation fails, the server will log an error and stop
Example:
{
"server": {
"host": "0.0.0.0",
"port": 8080,
"protocol": "mtls",
"cert_file": "./certs/server.crt",
"key_file": "./certs/server.key",
"ca_cert_file": "./certs/ca.crt",
"crl_file": "./certs/server.crl",
"use_system_ca": false,
"log_dir": "./logs"
}
}
2. client Section
Purpose: Client configuration (for connecting to external servers)
| Field | Type | Required | Description | Allowed Values |
|---|---|---|---|---|
enabled |
boolean | NO | Enable client configuration | true, false (default: false) |
protocol |
string | Conditional | Client protocol | "http", "https", "mtls" (default: "http") |
cert_file |
string | Conditional | Client certificate file path | Valid file path (required for mTLS when enabled) |
key_file |
string | Conditional | Client private key file path | Valid file path (required for mTLS when enabled) |
ca_cert_file |
string | Conditional | CA certificate file path | Valid file path (required for mTLS if use_system_ca=false) |
crl_file |
string | Optional | Certificate Revocation List file path | Valid CRL file path |
use_system_ca |
boolean | NO | Allow system CA store when ca_cert_file is not provided |
true, false (default: false) |
Protocol Requirements:
- HTTP: No certificates required
- HTTPS:
cert_fileandkey_fileare optional but recommended. If one is specified, both must be provided. - mTLS:
cert_fileandkey_fileare required whenenabled=true.ca_cert_fileis required ifuse_system_ca=false(default).
CRL Validation:
- Same validation rules as
serversection - If
crl_fileis specified and validation fails, the client connection will fail
Example:
{
"client": {
"enabled": true,
"protocol": "mtls",
"cert_file": "./certs/client.crt",
"key_file": "./certs/client.key",
"ca_cert_file": "./certs/ca.crt",
"crl_file": "./certs/client.crl",
"use_system_ca": false
}
}
3. registration Section (Deprecated - See Section 7 for Current Format)
⚠️ DEPRECATED pointer, not a second copy: this used to duplicate
Section 7's own field tables and example almost verbatim (bug 33c8d339
follow-up - two copies of the same shape drift out of sync over time).
See Section 7 (registration Section) above for the field tables,
nested registration.ssl/registration.heartbeat sections, protocol
requirements, and a full example - all still current.
4. auth Section
Purpose: Authentication and authorization configuration
| Field | Type | Required | Description | Allowed Values |
|---|---|---|---|---|
use_token |
boolean | NO | Enable token-based authentication | true, false (default: false) |
use_roles |
boolean | NO | Enable role-based authorization | true, false (default: false) |
tokens |
object | Conditional | Token-to-role mapping | Object with token strings as keys and role arrays as values (required if use_token=true) |
roles |
object | Conditional | Role-to-command mapping | Object with role strings as keys and command arrays as values (required if use_roles=true) |
Note: use_roles requires use_token=true
Example:
{
"auth": {
"use_token": true,
"use_roles": true,
"tokens": {
"admin-secret-key": ["mcpproxy"],
"user-secret-key": ["other"],
"readonly-secret-key": ["chunker"]
},
"roles": {
"mcpproxy": ["*"],
"other": ["health", "echo"],
"chunker": ["health"]
}
}
}
Complete SimpleConfig Example
{
"server": {
"host": "0.0.0.0",
"port": 8080,
"protocol": "mtls",
"cert_file": "./certs/server.crt",
"key_file": "./certs/server.key",
"ca_cert_file": "./certs/ca.crt",
"crl_file": "./certs/server.crl",
"use_system_ca": false,
"log_dir": "./logs"
},
"client": {
"enabled": true,
"protocol": "mtls",
"cert_file": "./certs/client.crt",
"key_file": "./certs/client.key",
"ca_cert_file": "./certs/ca.crt",
"crl_file": "./certs/client.crl",
"use_system_ca": false
},
"registration": {
"enabled": true,
"protocol": "mtls",
"register_url": "https://localhost:3005/register",
"unregister_url": "https://localhost:3005/unregister",
"server_id": "my-server-001",
"server_name": "My MCP Server",
"instance_uuid": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"auto_on_startup": true,
"auto_on_shutdown": true,
"ssl": {
"cert": "./certs/registration.crt",
"key": "./certs/registration.key",
"ca": "./certs/ca.crt",
"crl": "./certs/registration.crl",
"check_hostname": false
},
"heartbeat": {
"url": "https://localhost:3005/proxy/heartbeat",
"interval": 30
}
},
"auth": {
"use_token": false,
"use_roles": false,
"tokens": {},
"roles": {}
}
}
CRL Validation Details
Certificate Revocation List (CRL) validation is performed for all sections (server, client, registration) when a crl_file is specified:
- File Existence: The CRL file must exist and be accessible
- Format Validation: The file must be a valid CRL in PEM or DER format
- Expiration Check: The CRL must not be expired (checked against the
next_updatefield) - Certificate Revocation Check: If the CRL is valid, certificates are checked against it to ensure they are not revoked
Error Handling:
- If CRL file is specified but not found: Error logged, server stops
- If CRL file is not a valid CRL format: Error logged, server stops
- If CRL is expired: Error logged, server stops
- If certificate is revoked according to CRL: Error logged, server stops
CRL Validation Process:
- Check file exists → If not: Error and stop
- Validate CRL format (PEM/DER) → If invalid: Error and stop
- Check CRL expiration (
next_update) → If expired: Error and stop - Check certificate serial number against CRL → If revoked: Error and stop
Generating SimpleConfig
Use the adapter-cfg-gen command to generate SimpleConfig files:
# Generate HTTP configuration
adapter-cfg-gen --protocol http --out config.json
# Generate HTTPS configuration with server certificates
adapter-cfg-gen --protocol https \
--server-cert-file ./certs/server.crt \
--server-key-file ./certs/server.key \
--out config.json
# Generate mTLS configuration with all three sections
adapter-cfg-gen --protocol mtls \
--server-cert-file ./certs/server.crt \
--server-key-file ./certs/server.key \
--server-ca-cert-file ./certs/ca.crt \
--server-crl-file ./certs/server.crl \
--client-enabled \
--client-protocol mtls \
--client-cert-file ./certs/client.crt \
--client-key-file ./certs/client.key \
--client-ca-cert-file ./certs/ca.crt \
--client-crl-file ./certs/client.crl \
--with-proxy \
--registration-protocol mtls \
--registration-cert-file ./certs/registration.crt \
--registration-key-file ./certs/registration.key \
--registration-ca-cert-file ./certs/ca.crt \
--registration-crl-file ./certs/registration.crl \
--out config.json
Validating SimpleConfig
Use the adapter-cfg-val command to validate SimpleConfig files:
# Validate configuration file
adapter-cfg-val --file config.json
The validator checks:
- Required fields are present
- File paths exist and are accessible
- Certificate-key pairs match
- Certificates are not expired
- CRL files are valid and not expired
- Certificates are not revoked according to CRL
- Certificate chains are valid
Built-in Commands
health- Server health checkecho- Echo test commandconfig- Configuration managementhelp- Command help and documentationreload- Configuration reloadsettings- Settings managementload/unload- Command loading/unloadingplugins- Plugin managementproxy_registration- Proxy registration controltransport_management- Transport protocol managementrole_test- Role-based access testing
Custom Commands with Queue Execution
Commands that use use_queue=True execute in child processes via the queue system. This is essential for:
- CUDA compatibility: CUDA requires multiprocessing spawn mode (not fork)
- Long-running tasks: Non-blocking execution of time-consuming operations
- Resource isolation: Commands run in separate processes
⚠️ Critical: Spawn Mode Registration
When using use_queue=True, commands execute in child processes (spawn mode). Child processes start with a fresh Python interpreter and do not inherit the parent process's command registry. You must ensure commands are registered in child processes.
Registration Methods
Method 1: Module-Level Auto-Registration (Recommended)
Register commands automatically when the module is imported:
# In your_command_module.py
from mcp_proxy_adapter.commands.command_registry import registry
from mcp_proxy_adapter.commands.base import Command, CommandResult
class MyQueueCommand(Command):
"""Command that executes via queue."""
name = "my_queue_command"
descr = "My queue command"
use_queue = True # Enable queue execution
async def execute(self, message: str = "default", **kwargs) -> CommandResult:
"""Execute command."""
return CommandResult(success=True, data={"message": message})
def _auto_register_commands():
"""Auto-register commands when module is imported."""
try:
registry.get_command("my_queue_command")
except KeyError:
registry.register(MyQueueCommand, "custom")
# Execute on import
_auto_register_commands()
Then register the module for auto-import:
# In your main.py or application entry point
from mcp_proxy_adapter.commands.hooks import register_auto_import_module
# Register module for auto-import in child processes
register_auto_import_module("your_package.your_command_module")
Method 2: Hook-Based Registration (Automatic Module Path Extraction)
When you register a hook function, the module path is automatically extracted and stored:
# In your main.py
from mcp_proxy_adapter.commands.hooks import register_custom_commands_hook
from mcp_proxy_adapter.commands.command_registry import registry
def register_my_commands(registry_instance):
"""Register custom commands via hook."""
from your_package.your_command_module import MyQueueCommand
registry_instance.register(MyQueueCommand, "custom")
# Register hook - module path is automatically extracted
register_custom_commands_hook(register_my_commands)
# Also register in main process
registry.register(MyQueueCommand, "custom")
The adapter automatically:
- Extracts the module path from the hook function (
your_package.your_command_module) - Stores it for auto-import in child processes
- Imports the module in child processes before command execution
Method 3: Environment Variable (Fallback)
Set the MCP_AUTO_REGISTER_MODULES environment variable:
export MCP_AUTO_REGISTER_MODULES="your_package.your_command_module,another.module"
Or in your code:
import os
os.environ['MCP_AUTO_REGISTER_MODULES'] = 'your_package.your_command_module'
Complete Example
# commands/my_queue_command.py
"""
Author: Your Name
email: your.email@example.com
Queue command example with proper registration for spawn mode.
"""
import asyncio
from typing import Any, Dict
from mcp_proxy_adapter.commands.base import Command, CommandResult
from mcp_proxy_adapter.commands.command_registry import registry
class LongRunningCommand(Command):
"""
Long-running command that executes via queue.
This command:
- Executes in a child process (spawn mode)
- Supports progress tracking
- Returns result when completed
"""
name = "long_running_task"
descr = "Long-running task with progress updates (executes via queue)"
use_queue = True # Enable automatic queue execution
async def execute(
self,
task_name: str = "default_task",
duration: int = 60,
steps: int = 10,
**kwargs,
) -> CommandResult:
"""Execute long-running task with progress updates."""
step_duration = duration / steps
# Simulate work with steps
for i in range(steps):
await asyncio.sleep(step_duration)
return CommandResult(
success=True,
data={
"task_name": task_name,
"duration": duration,
"steps_completed": steps,
"status": "completed",
"message": f"Task '{task_name}' completed successfully after {duration} seconds",
},
)
def _auto_register_commands():
"""Auto-register commands when module is imported."""
try:
registry.get_command("long_running_task")
except KeyError:
registry.register(LongRunningCommand, "custom")
# Execute on import
_auto_register_commands()
# main.py
from mcp_proxy_adapter.commands.command_registry import registry
from mcp_proxy_adapter.commands.hooks import register_auto_import_module
from commands.my_queue_command import LongRunningCommand
def register_all_commands():
"""Register all commands."""
# Register in main process
registry.register(LongRunningCommand, "custom")
# Register module for auto-import in child processes (spawn mode)
register_auto_import_module("commands.my_queue_command")
print("✅ Long-running command registered (with spawn mode support)")
if __name__ == "__main__":
register_all_commands()
# Start server...
Verification
To verify your command works correctly in spawn mode:
-
Check command is registered in main process:
from mcp_proxy_adapter.commands.command_registry import registry assert registry.command_exists("my_queue_command")
-
Test queue execution:
# Execute command via JSON-RPC response = requests.post( "http://localhost:8080/api/jsonrpc", json={ "jsonrpc": "2.0", "method": "my_queue_command", "params": {"message": "test"}, "id": 1 } ) result = response.json() job_id = result["result"]["job_id"] # Check job status status_response = requests.post( "http://localhost:8080/api/jsonrpc", json={ "jsonrpc": "2.0", "method": "job_status", "params": {"job_id": job_id}, "id": 2 } )
Troubleshooting
Error: "Command 'my_command' not found"
This indicates the command is not registered in the child process. Solutions:
-
Ensure module-level auto-registration:
- Add
_auto_register_commands()function to your module - Call it at module level (not inside a function)
- Add
-
Register module for auto-import:
register_auto_import_module("your_package.your_command_module")
-
Check module path:
- Use full module path (e.g.,
"my_package.commands.my_command") - Ensure the module can be imported (no circular imports)
- Use full module path (e.g.,
-
Use environment variable:
export MCP_AUTO_REGISTER_MODULES="your_package.your_command_module"
Command executes but fails in child process
- Check that all dependencies are available in child process
- Ensure CUDA/GPU resources are initialized in child process (not parent)
- Verify multiprocessing start method is
spawn(required for CUDA)
Automatic PYTHONPATH Management (6.9.89+)
✅ NEW in 6.9.89+: The adapter automatically manages PYTHONPATH for spawn mode!
The adapter now:
- Automatically adds application root to
PYTHONPATHbased onconfig_path - Automatically adds registered module paths to
PYTHONPATH - Updates environment variable so child processes inherit the paths
- Retries imports with enhanced path resolution if initial import fails
You no longer need to manually modify PYTHONPATH or sys.path!
The adapter handles this automatically during server startup. If you still encounter import errors, check the logs for detailed information about PYTHONPATH and sys.path.
Best Practices
- Always use module-level auto-registration for commands with
use_queue=True - Register modules explicitly using
register_auto_import_module() - Let the adapter manage PYTHONPATH - no manual path manipulation needed
- Test in spawn mode before deploying to production
- Use idempotent registration (check if command exists before registering)
- Document registration requirements in your command module docstrings
Troubleshooting Import Errors
If you see ModuleNotFoundError in child process logs:
-
Check logs for PYTHONPATH information:
CommandExecutionJob: Could not import module embed.commands: No module named 'embed' PYTHONPATH=/path/to/project sys.path (first 5)=[...] -
Verify module is registered:
from mcp_proxy_adapter.commands.hooks import hooks print(hooks.get_auto_import_modules()) # Should include your module
-
Check application path:
- Ensure
config_pathis provided tocreate_app()orcreate_and_run_server() - The adapter uses
config_pathto determine application root
- Ensure
-
Manual override (if needed):
import os os.environ['PYTHONPATH'] = '/path/to/project:' + os.environ.get('PYTHONPATH', '')
Security Features
- Authentication: API keys, JWT tokens, certificate-based auth
- Authorization: Role-based permissions with wildcard support
- SSL/TLS: Full SSL/TLS and mTLS support
- Rate Limiting: Configurable request rate limiting
- Security Headers: Automatic security header injection
Examples
The mcp_proxy_adapter/examples/ directory contains comprehensive examples for different use cases:
- Basic Framework: Simple HTTP server setup
- Full Application: Complete application with custom commands and hooks
- Security Testing: Comprehensive security test suite
- Certificate Generation: SSL/TLS certificate management
Which client should I use?
For your own code, import the client library directly - do not copy
examples/universal_client.py or examples/demo_client.py, which are thin
demo wrappers over it, kept only to show the config shape and call sequence:
mcp_proxy_adapter.core.client.UniversalClient(andmcp_proxy_adapter.core.client.create_client_from_config) for the none/api_key/jwt/basic auth matrix plus TLS/mTLS via thesecurity.ssl/security.certificateconfig keys.mcp_proxy_adapter.client.jsonrpc_client.client.JsonRpcClientfor queue-backed commands with automatic polling (see Queue-Backed Commands above).
Test Environment Setup
The framework includes a comprehensive test environment setup that automatically creates configurations, generates certificates, and runs tests:
# Create a complete test environment with all configurations and certificates
python -m mcp_proxy_adapter.examples.setup_test_environment
# Create test environment in a specific directory
python -m mcp_proxy_adapter.examples.setup_test_environment /path/to/test/dir
# Skip certificate generation (use existing certificates)
python -m mcp_proxy_adapter.examples.setup_test_environment --skip-certs
# Skip running tests (setup only)
python -m mcp_proxy_adapter.examples.setup_test_environment --skip-tests
Docker: mTLS proxy registration
Step-by-step registration of a backend to mcp-proxy over mTLS inside Docker (compose, certificates, SERVER_PUBLIC_URL, verification): docs/EN/guides/registration_with_mcp_proxy_in_docker_mtls.md.
Configuration Generation
Generate test configurations from a comprehensive template:
# Generate all test configurations
python -m mcp_proxy_adapter.examples.create_test_configs
# Generate from specific comprehensive config
python -m mcp_proxy_adapter.examples.create_test_configs --comprehensive-config config.json
# Generate specific configuration types
python -m mcp_proxy_adapter.examples.create_test_configs --types http,https,mtls
Certificate Generation
Generate SSL/TLS certificates for testing:
# Generate all certificates using mcp_security_framework
python -m mcp_proxy_adapter.examples.generate_all_certificates
# Generate certificates with custom configuration
python -m mcp_proxy_adapter.examples.generate_certificates_framework --config cert_config.json
Security Testing
Run comprehensive security tests:
# Run all security tests
python -m mcp_proxy_adapter.examples.run_security_tests_fixed
# Run full test suite (includes setup, config generation, certificate generation, and testing)
python -m mcp_proxy_adapter.examples.run_full_test_suite
Complete Workflow Example
# 1. Install the package
pip install mcp-proxy-adapter
# 2. Create test environment (automatically runs tests)
python -m mcp_proxy_adapter.examples.setup_test_environment
# 3. Or run individual steps:
# Generate certificates
python -m mcp_proxy_adapter.examples.generate_all_certificates
# Generate configurations
python -m mcp_proxy_adapter.examples.create_test_configs
# Run security tests
python -m mcp_proxy_adapter.examples.run_security_tests_fixed
# 4. Start server with generated configuration
python -m mcp_proxy_adapter --config configs/http_simple.json
Deprecations
mcp_proxy_adapter.core.proxy(includingauth_manager.AuthManager) is deprecated and will be removed in the next major release. This package used to hold a legacy proxy-registration stack (ssl_manager.py,registration_client.py,proxy_registration_manager.py); those modules have already been removed as dead code, and onlyauth_manager.AuthManagerremains, as a narrow compatibility shim for external consumers that still import it directly. Importing it emits aDeprecationWarning.- Replacement for registration: build it through
mcp_proxy_adapter/api/core/registration_manager. - Replacement for client auth headers: build them through the
client's own auth handling
(
ClientSecurityManager.get_client_auth_headers(), or themcp_proxy_adapter/client/jsonrpc_clientpackage).
- Replacement for registration: build it through
Development
The project follows a modular architecture:
mcp_proxy_adapter/api/- FastAPI application and handlersmcp_proxy_adapter/commands/- Command system and built-in commandsmcp_proxy_adapter/core/- Core functionality and utilitiesmcp_proxy_adapter/config.py- Configuration management
License
This project is licensed under the MIT License.
Support
For issues and questions, please contact vasilyvz@gmail.com.
Release files for mcp-proxy-adapter 8.10.61
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mcp_proxy_adapter-8.10.61.tar.gz | 696.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcp_proxy_adapter-8.10.61-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.6 MB
Release files / mcp_proxy_adapter-8.10.61.tar.gz
| Download URL | mcp_proxy_adapter-8.10.61.tar.gz |
|---|---|
| Size | 696.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
329e60cc32bf54049339341c19e2e251ac725a2fc55481b054d1036b1a46f9b9
|
|
BLAKE2b-256 checksum How to use checksums |
cf477464d74c1a71147635ef53c33465050e08c2b43f77bfed04077c5adc6c3e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Release files / mcp_proxy_adapter-8.10.61-py3-none-any.whl
| Download URL | mcp_proxy_adapter-8.10.61-py3-none-any.whl |
|---|---|
| Size | 901.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d2e148d7ebfb94b1b13380daa9e2f5dbe8dcd11eb59e0aff5a99d3406dccee90
|
|
BLAKE2b-256 checksum How to use checksums |
5d82c5c46bf29ac987d9e9119083b10833fefc6bfd4770f568631ea9e36f9696
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|