Multi-server API Gateway for FastMCP Servers, providing REST endpoints for frontend applications and external services to interact with multiple MCP servers.
Frontend Chatbot → REST API (Proxy Server) → FastMCP Python Client → Multiple MCP Servers
[API Key Auth] [HTTP Transport] [Configurable via YAML]
- 🚀 FastAPI - Modern, async web framework with auto-generated OpenAPI docs
- 🔐 API Key Authentication - Secure access control via
X-API-Keyheader with per-key server scoping - 🔌 FastMCP Client - Direct integration with FastMCP Python Client
- 🌐 Multi-Server Support - Connect to multiple MCP servers simultaneously
- 🛡️ Fine-Grained Access Control - Restrict API keys to specific MCP servers
- ⚙️ YAML Configuration - Easy server and API key management via YAML files
- 🎯 Flexible Routing - Choose server per request or use default
- 🔍 Server Discovery - API keys can discover their accessible servers
- 🌐 CORS Support - Configured for frontend applications
- 📊 Health Checks - Monitor proxy and all MCP server connectivity
- 🐳 Docker Ready - Containerized deployment
- 📝 Full MCP Protocol - Support for tools, resources, and prompts
- Python 3.11+
- pip
- Clone the repository and navigate to the project directory:
cd /Users/sevey/Code/ardaglobal/mcp-proxy- Create a virtual environment:
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate- Install dependencies:
pip install -r requirements.txt- Configure environment variables:
# Copy example file
cp .env.example .env
# Edit .env with your settings (optional - defaults are provided)
# For quick development, the defaults work out of the boxCORS_ORIGINS or you'll get CORS errors!
- Run the server:
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000The server will be available at http://localhost:8000
Once running, access:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
Basic health check for the proxy server.
curl http://localhost:8000/healthDetailed MCP server connectivity check with optional RTT (Round-Trip Time) measurement.
Check all enabled servers:
curl http://localhost:8000/health/mcpCheck specific server:
curl "http://localhost:8000/health/mcp?server_name=arda-insights"Measure network latency (RTT):
# Add measure_rtt=true to diagnose latency between proxy and upstream servers
curl "http://localhost:8000/health/mcp?measure_rtt=true"Response with RTT:
{
"status": "healthy",
"servers": [
{
"name": "arda-insights",
"status": "healthy",
"rtt": {
"rtt_ms": 417.93,
"rtt_min_ms": 324.33,
"rtt_max_ms": 510.80,
"samples": 5
}
}
],
"aggregate_rtt": {
"avg_rtt_ms": 423.99,
"servers_measured": 2
}
}Note: RTT measurement is optional and isolated to health checks only - it does not affect production tool execution performance.
All server endpoints require authentication via X-API-Key header.
List all configured MCP servers with their connection status.
With authentication:
curl -H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/servers/listWithout authentication (AUTH_ENABLED=false):
curl http://localhost:8000/api/v1/servers/listResponse:
{
"success": true,
"default_server": "arda-insights",
"count": 2,
"servers": [
{
"name": "arda-insights",
"url": "https://arda-insights.fastmcp.app/mcp",
"type": "production",
"description": "Arda Global insights and analytics server",
"enabled": true,
"connected": true
}
]
}Get detailed information about a specific MCP server including available tools.
curl -H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/servers/arda-insightsDiscover which MCP servers are accessible with the current API key.
This endpoint returns only the servers that the authenticated API key has permission to access. This is useful for frontend applications to dynamically discover available servers.
curl -H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/servers/discoveryResponse:
{
"success": true,
"api_key_name": "platform-app",
"api_key_description": "Frontend platform application",
"server_count": 3,
"allowed_servers": [
{
"name": "arda-insights",
"type": "production",
"description": "Arda Global insights and analytics server",
"enabled": true
},
{
"name": "arda-credit-api",
"type": "staging",
"description": "Arda Credit API server",
"enabled": true
}
]
}All tool endpoints require authentication via X-API-Key header. Tools execute actions and return results.
List all available tools from an MCP server.
List tools from default server:
curl -H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/tools/listList tools from specific server:
curl -H "X-API-Key: your-api-key" \
"http://localhost:8000/api/v1/tools/list?server_name=arda-insights"Call a specific tool with arguments on an MCP server.
Call tool on default server:
curl -X POST http://localhost:8000/api/v1/tools/call \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"tool_name": "semantic_search",
"arguments": {
"query": "authentication logic",
"collection_name": "arda_code_rust",
"limit": 10
}
}'Call tool on specific server:
curl -X POST http://localhost:8000/api/v1/tools/call \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"tool_name": "semantic_search",
"server_name": "arda-insights",
"arguments": {
"query": "authentication logic",
"collection_name": "arda_code_rust",
"limit": 10
}
}'Execute multiple tool calls in sequence.
Batch calls on default server:
curl -X POST http://localhost:8000/api/v1/tools/batch \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"calls": [
{
"tool_name": "semantic_search",
"arguments": {"query": "error handling"}
},
{
"tool_name": "semantic_search",
"arguments": {"query": "database queries"}
}
]
}'Batch calls with server selection:
curl -X POST http://localhost:8000/api/v1/tools/batch \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"server_name": "arda-insights",
"calls": [
{
"tool_name": "semantic_search",
"arguments": {"query": "error handling"}
},
{
"tool_name": "health_check",
"server_name": "arda-dev",
"arguments": {}
}
]
}'Note: Individual calls can override the batch-level server_name.
All prompt endpoints require authentication via X-API-Key header. Prompts return message templates that can be used to structure conversations with language models.
Key Difference from Tools:
- Tools execute actions (search databases, call APIs, process data) and return results
- Prompts return pre-defined message templates with dynamic arguments for conversation structuring
List all available prompts from an MCP server.
List prompts from specific server:
curl -H "X-API-Key: your-api-key" \
"http://localhost:8000/api/v1/prompts/list?server_name=arda-insights"List prompts from all accessible servers:
curl -H "X-API-Key: your-api-key" \
http://localhost:8000/api/v1/prompts/listResponse:
{
"success": true,
"server_name": "arda-insights",
"count": 3,
"prompts": [
{
"name": "code_review",
"description": "Generate a code review prompt",
"arguments": [
{
"name": "language",
"description": "Programming language",
"required": true
},
{
"name": "focus",
"description": "What to focus on",
"required": false
}
]
}
]
}Get a specific prompt with arguments. Returns rendered message templates.
Get prompt from specific server:
curl -X POST http://localhost:8000/api/v1/prompts/get \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"prompt_name": "code_review",
"server_name": "arda-insights",
"arguments": {
"language": "rust",
"focus": "security"
}
}'Response:
{
"success": true,
"prompt_name": "code_review",
"server_name": "arda-insights",
"result": {
"description": "Code review prompt for Rust with security focus",
"messages": [
{
"role": "user",
"content": "Please review this Rust code with a focus on security..."
}
]
}
}Note: All prompt arguments must be strings (per MCP protocol specification).
Execute multiple prompt gets in sequence.
curl -X POST http://localhost:8000/api/v1/prompts/batch \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"calls": [
{
"prompt_name": "code_review",
"server_name": "arda-insights",
"arguments": {"language": "rust"}
},
{
"prompt_name": "debug_guide",
"server_name": "arda-insights",
"arguments": {"error_type": "panic"}
}
]
}'const API_BASE = 'http://localhost:8000';
const API_KEY = 'your-api-key';
// List available MCP servers
async function listServers() {
const response = await fetch(`${API_BASE}/api/v1/servers/list`, {
headers: { 'X-API-Key': API_KEY }
});
return response.json();
}
// List available tools from a server
async function listTools(serverName = null) {
const url = serverName
? `${API_BASE}/api/v1/tools/list?server_name=${serverName}`
: `${API_BASE}/api/v1/tools/list`;
const response = await fetch(url, {
headers: { 'X-API-Key': API_KEY }
});
return response.json();
}
// List available prompts from a server
async function listPrompts(serverName = null) {
const url = serverName
? `${API_BASE}/api/v1/prompts/list?server_name=${serverName}`
: `${API_BASE}/api/v1/prompts/list`;
const response = await fetch(url, {
headers: { 'X-API-Key': API_KEY }
});
return response.json();
}
// Call a tool on a specific server
async function callTool(toolName, args, serverName = null) {
const response = await fetch(`${API_BASE}/api/v1/tools/call`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
tool_name: toolName,
arguments: args,
server_name: serverName // Optional: uses default if not provided
})
});
return response.json();
}
// Get a prompt from a specific server
async function getPrompt(promptName, args, serverName) {
const response = await fetch(`${API_BASE}/api/v1/prompts/get`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
prompt_name: promptName,
arguments: args, // Must be string values
server_name: serverName
})
});
return response.json();
}
// Example usage
// List all servers
const servers = await listServers();
console.log('Available servers:', servers);
// Call tool on default server
const result1 = await callTool('semantic_search', {
query: 'authentication logic',
collection_name: 'arda_code_rust'
});
console.log('Result from default server:', result1);
// Call tool on specific server
const result2 = await callTool('semantic_search', {
query: 'database queries',
collection_name: 'arda_code_typescript'
}, 'arda-insights');
console.log('Result from arda-insights:', result2);
// Get a prompt
const prompt = await getPrompt('code_review', {
language: 'typescript',
focus: 'performance'
}, 'arda-insights');
console.log('Prompt messages:', prompt.result.messages);The proxy supports multiple MCP servers configured via a YAML file. Create or edit servers.yaml in the project root:
# MCP Server Configuration
servers:
- name: arda-insights
url: https://arda-insights.fastmcp.app/mcp
type: production
description: Arda Global insights and analytics server
timeout: 30.0
enabled: true
- name: arda-dev
url: http://localhost:8001/mcp
type: development
description: Development MCP server for testing
timeout: 30.0
enabled: false
- name: custom-mcp
url: https://custom.example.com/mcp
type: production
description: Custom MCP server
timeout: 45.0
enabled: true
# Default server to use when no server_name is specified in requests
default_server: arda-insightsServer Fields:
name- Unique identifier for the server (used in API requests)url- MCP server endpoint URLtype- Server type (e.g., production, development, staging)description- Human-readable descriptiontimeout- Connection timeout in secondsenabled- Whether the server is active (disabled servers won't connect on startup)
Default Server:
When API requests don't specify a server_name, the proxy uses the server defined in default_server.
| Variable | Description | Default |
|---|---|---|
APP_NAME |
Application name | "MCP Proxy Server" |
APP_VERSION |
Application version | "0.1.0" |
DEBUG |
Enable debug mode | false |
SERVERS_CONFIG_FILE |
Host path to servers YAML file (Docker only) | "./servers.yaml" |
SERVERS_CONFIG_PATH |
Container/app path to servers YAML config | "servers.yaml" |
API_KEYS_CONFIG_PATH |
Path to API keys YAML configuration file | "api-keys.yaml" |
AUTH_ENABLED |
Enable API key authentication | true |
API_KEYS |
JSON array of valid API keys (legacy, full server access) | ["dev-key-123"] |
CORS_ORIGINS |
JSON array of allowed origins (use "*" for all origins) | [] (empty - blocks all) |
CORS_ALLOW_CREDENTIALS |
Allow credentials in CORS requests | true |
CORS_ALLOW_METHODS |
Allowed HTTP methods | "*" |
CORS_ALLOW_HEADERS |
Allowed HTTP headers | "*" |
LOG_LEVEL |
Logging level | "INFO" |
The MCP Proxy supports two methods of API key management:
- YAML-based configuration (Recommended) - Fine-grained server access control per API key
- Environment variable (Legacy) - Simple list of keys with full access to all servers
Create an api-keys.yaml file to define API keys with scoped server access:
# api-keys.yaml
api_keys:
# Platform API key - limited access
platform-key-abc123:
name: "platform-app"
description: "Frontend platform application"
allowed_servers:
- arda-insights
- arda-credit-api
- ardaglobal-perplexity
created_at: "2025-01-15"
# Internal API key - full access
internal-key-xyz789:
name: "internal-app"
description: "Internal application with full server access"
allowed_servers: "*" # Special value: access to all servers
created_at: "2025-01-15"API Key Fields:
- key (YAML key) - The actual API key value used in
X-API-Keyheader name- Human-readable name for the API keydescription- Purpose or owner of the API keyallowed_servers- List of server names this key can access, or"*"for all serverscreated_at- Creation date for tracking
Server Access Control:
- API keys can only access servers listed in their
allowed_serversfield - Use
"*"to grant access to all configured servers (use sparingly) - Server names must match those defined in
servers.yaml - Attempting to access unauthorized servers returns HTTP 403 Forbidden
Example Usage:
# Create api-keys.yaml from example
cp api-keys.yaml.example api-keys.yaml
# Edit with your API keys
vim api-keys.yaml
# Test with platform key (limited access)
curl -H "X-API-Key: platform-key-abc123" \
http://localhost:8000/api/v1/servers/discovery
# This will succeed (has access)
curl -X POST http://localhost:8000/api/v1/tools/call \
-H "X-API-Key: platform-key-abc123" \
-H "Content-Type: application/json" \
-d '{"tool_name": "search", "server_name": "arda-insights", "arguments": {}}'
# This will fail with 403 (no access)
curl -X POST http://localhost:8000/api/v1/tools/call \
-H "X-API-Key: platform-key-abc123" \
-H "Content-Type: application/json" \
-d '{"tool_name": "search", "server_name": "internal-only-server", "arguments": {}}'Benefits:
- ✅ Fine-grained access control per API key
- ✅ Different keys for different applications/environments
- ✅ Security through least-privilege access
- ✅ Easy to audit which keys have access to which servers
- ✅ Server discovery endpoint shows only accessible servers
For backward compatibility and simple deployments, you can still use environment variables:
AUTH_ENABLED=true
API_KEYS='["dev-key-123","prod-key-abc"]'Note: Keys defined via environment variables have full access to all servers (equivalent to allowed_servers: "*").
Priority: If api-keys.yaml exists, keys defined there take precedence. Environment variable keys serve as a fallback.
For quick local development, you can completely disable authentication:
# In .env or docker-compose.yml
AUTH_ENABLED=falseWhen auth is disabled:
- No
X-API-Keyheader required - All endpoints are publicly accessible
- All API keys have full access to all servers
- Startup logs will show:
⚠️ AUTHENTICATION IS DISABLED - Root endpoint (
/) will show"auth_enabled": false
For production, always use YAML-based API key configuration:
- Create
api-keys.yamlwith production keys and appropriate server access - Ensure
AUTH_ENABLED=true(default) - Mount
api-keys.yamlin Docker deployments - Use secure, randomly-generated API keys
- Follow least-privilege principle - grant minimum necessary server access
# Production example
AUTH_ENABLED=true
API_KEYS_CONFIG_PATH=api-keys.yamlThe Docker image is configuration-agnostic - servers.yaml is not built into the image. This makes the image portable and reusable across different environments.
docker build -t mcp-proxy-server .Important: You must mount servers.yaml at runtime via volume mount:
docker run -d \
--name mcp-proxy \
-p 8000:8000 \
-v $(pwd)/servers.yaml:/app/servers.yaml \
-e AUTH_ENABLED=true \
-e API_KEYS='["your-secure-api-key"]' \
-e CORS_ORIGINS='["https://yourdomain.com"]' \
mcp-proxy-serverNote: The container will fail to start if servers.yaml is not provided.
The included docker-compose.yml automatically mounts servers.yaml and uses environment variable substitution with sensible defaults.
Benefits of Runtime Configuration:
- ✅ One image, multiple configs - Use the same Docker image for dev/staging/prod
- ✅ No rebuild needed - Update servers without rebuilding the image
- ✅ Configuration as code - Keep
servers.yamlin version control separate from the image - ✅ Easier CI/CD - Build once, deploy everywhere with different configs
Option 1: Use defaults (for development)
docker-compose up -dOption 2: Override with environment variables
# Set environment variables
export AUTH_ENABLED=true
export API_KEYS='["prod-key-abc","prod-key-xyz"]'
export LOG_LEVEL=DEBUG
# Start with your custom config
docker-compose up -dOption 3: Use .env file
# Create .env file (docker-compose will automatically load it)
cat > .env << EOF
AUTH_ENABLED=true
API_KEYS=["prod-key-abc"]
CORS_ORIGINS=["https://yourdomain.com"]
LOG_LEVEL=WARNING
EOF
docker-compose up -dOption 4: Use custom servers config file
# Use a different servers configuration file
export SERVERS_CONFIG_FILE=./config/production-servers.yaml
docker-compose up -d
# Or set in .env file
echo "SERVERS_CONFIG_FILE=./config/production-servers.yaml" >> .env
docker-compose up -dThe docker-compose.yml includes all available environment variables with defaults:
version: '3.8'
services:
mcp-proxy:
build: .
ports:
- "8000:8000"
volumes:
# Both source and destination paths support environment variables
- ${SERVERS_CONFIG_FILE:-./servers.yaml}:/app/${SERVERS_CONFIG_PATH:-servers.yaml}:ro
environment:
- APP_NAME=${APP_NAME:-MCP Proxy Server}
- DEBUG=${DEBUG:-false}
- SERVERS_CONFIG_PATH=${SERVERS_CONFIG_PATH:-servers.yaml}
- AUTH_ENABLED=${AUTH_ENABLED:-false}
- API_KEYS=${API_KEYS:-["dev-key-123"]}
- CORS_ORIGINS=${CORS_ORIGINS:-["http://localhost:3000"]}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
restart: unless-stoppedIf you're upgrading from an older version that used MCP_SERVER_URL environment variable:
-
Create
servers.yamlconfiguration file with your server(s):servers: - name: my-server url: https://your-mcp-server.com/mcp # Your old MCP_SERVER_URL value type: production description: My MCP server timeout: 30.0 enabled: true default_server: my-server
-
Remove old environment variables from
.env:MCP_SERVER_URL(no longer used)MCP_TIMEOUT(now configured per-server inservers.yaml)
-
Update your API calls (optional - backwards compatible):
- Existing calls without
server_namewill use the default server - To specify a server, add
server_namefield to request body:{ "tool_name": "your_tool", "server_name": "my-server", "arguments": {...} }
- Existing calls without
-
Test the migration:
# Check server configuration loaded correctly curl http://localhost:8000/ # List configured servers curl -H "X-API-Key: your-key" http://localhost:8000/api/v1/servers/list # Health check curl http://localhost:8000/health/mcp
Note: The proxy is fully backwards compatible. Existing API clients will continue to work without modifications using the default server.
# Install dev dependencies
pip install pytest pytest-asyncio httpx
# Run tests
pytestpip install black isort
black app/
isort app/pip install mypy
mypy app/Application logs are written to stdout with structured formatting:
2024-01-01 12:00:00 - app.main - INFO - Starting MCP Proxy Server...
2024-01-01 12:00:01 - app.mcp_client - INFO - Connecting to MCP server at https://arda-insights.fastmcp.app/mcp
2024-01-01 12:00:02 - app.mcp_client - INFO - Successfully connected to MCP server
Set up monitoring tools to periodically check:
GET /health- Proxy server healthGET /health/mcp- MCP server connectivity
If you see "Servers configuration file not found":
- Ensure
servers.yamlexists in the project root - Check the
SERVERS_CONFIG_PATHenvironment variable points to the correct file - Verify the YAML syntax is correct
If you see "Server 'xxx' not found in configuration":
- Check the server name in your
servers.yamlfile - Ensure the server name matches exactly (case-sensitive)
- Verify the server is enabled (
enabled: true)
If you see "MCP server is unreachable":
- Check the server URL in
servers.yamlis correct - Verify network connectivity to the MCP server
- Check MCP server is running:
curl <server-url> - Review server timeout settings in
servers.yaml - Check logs for specific connection errors
If specific servers fail to connect on startup:
- Check
/api/v1/servers/listto see which servers are connected - Try health check for specific server:
/health/mcp?server_name=xxx - Verify the server is enabled in
servers.yaml - Check server logs for authentication or network issues
If you get 401 Unauthorized:
- Ensure
X-API-Keyheader is included in requests - Verify the API key matches one in
API_KEYSenvironment variable - Check for typos or extra whitespace in the key
The proxy is secure by default - if CORS_ORIGINS is not set, all origins are blocked. You must explicitly configure CORS:
Development (permissive):
CORS_ORIGINS=["*"] # Explicitly allow all originsProduction (recommended):
CORS_ORIGINS=["https://yourdomain.com","https://app.yourdomain.com"]Default (secure):
# CORS_ORIGINS not set or empty = all origins blockedIf frontend gets CORS errors:
- Ensure
CORS_ORIGINSis set (it defaults to empty/blocked) - Set to
["*"]for development or specific origins for production - Ensure the origin URL matches exactly (including protocol and port)
- Restart the server after updating environment variables
- Resource endpoints (read MCP resources)
- Dynamic server configuration (add/remove servers via API without restart)
- Server load balancing and failover
- Server health monitoring with automatic reconnection
- JWT token validation alongside API keys
- WebSocket support for streaming responses
- Request caching for repeated queries
- Multi-tenant API key management with database
- Rate limiting and quotas per API key and per server
- Metrics dashboard (Prometheus/Grafana) with per-server metrics
- Request/response logging to database
- Admin API for key and server management
Internal use - Arda Global
For issues or questions, contact the Arda Global development team.