The MCP-SWAIG Gateway bridges Model Context Protocol (MCP) servers with SignalWire AI Gateway (SWAIG) functions, allowing SignalWire AI agents to interact with MCP-based tools. This gateway acts as a translation layer and session manager between the two protocols.
The MCP Gateway is included in the SignalWire Agents SDK. Install with the gateway dependencies:
pip install "signalwire-agents[mcp-gateway]"Once installed, the mcp-gateway CLI command is available:
mcp-gateway -c config.json-
MCP Gateway Service (
mcp_gateway/)- HTTP/HTTPS server with Basic Authentication
- Manages multiple MCP server instances
- Handles session lifecycle per SignalWire call
- Translates between SWAIG and MCP protocols
-
MCP Gateway Skill (
signalwire/skills/mcp_gateway/)- SignalWire skill that connects agents to the gateway
- Dynamically creates SWAIG functions from MCP tools
- Manages session lifecycle using call_id
-
Test MCP Server (
mcp_gateway/test/todo_mcp.py)- Simple todo list MCP server for testing
- Demonstrates stateful MCP server implementation
SignalWire Agent Gateway Service MCP Server
| | |
|---(1) Add Skill--------------->| |
|<--(2) Query Tools--------------| |
| |---(3) List Tools-------->|
| |<--(4) Tool List----------|
|---(5) Call SWAIG Function----->| |
| |---(6) Spawn Session----->|
| |---(7) Call MCP Tool----->|
| |<--(8) MCP Response-------|
|<--(9) SWAIG Response-----------| |
| | |
|---(10) Hangup Hook------------>| |
| |---(11) Close Session---->|
The gateway uses a custom envelope format for routing and session management:
{
"session_id": "call_xyz123", // From SWAIG call_id
"service": "todo", // MCP service name
"tool": "add_todo", // Tool name
"arguments": { // Tool arguments
"text": "Buy milk"
},
"timeout": 300, // Session timeout in seconds
"metadata": { // Optional metadata
"agent_id": "agent_123",
"timestamp": "2024-01-20T10:30:00Z"
}
}signalwire/mcp_gateway/ # Core gateway package (installed with SDK)
├── __init__.py # Package exports
├── gateway_service.py # Main HTTP/HTTPS server
├── mcp_manager.py # MCP server lifecycle management
└── session_manager.py # Session handling and timeouts
mcp_gateway/ # Configuration and deployment files
├── config.json # Gateway configuration
├── sample_config.json # Example configuration
├── Dockerfile # Docker container definition
├── docker-compose.yml # Docker compose configuration
├── mcp-docker.sh # Docker management helper script
├── README.md # Gateway documentation
├── certs/ # SSL certificates (optional)
│ └── .gitignore # Ignore actual certificates
├── test/
│ ├── todo_mcp.py # Test MCP server
│ ├── test_gateway.sh # Curl test scripts
│ └── test_agent.py # Test SignalWire agent
└── examples/
└── generate_cert.sh # Generate self-signed certificate
signalwire/skills/mcp_gateway/
├── __init__.py
├── skill.py # MCP gateway skill
└── README.md # Skill documentation
The configuration supports environment variable substitution using ${VAR_NAME|default} syntax:
{
"server": {
"host": "${MCP_HOST|0.0.0.0}",
"port": "${MCP_PORT|8080}",
"auth_user": "${MCP_AUTH_USER|admin}",
"auth_password": "${MCP_AUTH_PASSWORD|changeme}",
"auth_token": "${MCP_AUTH_TOKEN|optional-bearer-token}"
},
"services": {
"todo": {
"command": ["python3", "./test/todo_mcp.py"],
"description": "Simple todo list for testing",
"enabled": true,
"sandbox": {
"enabled": true,
"resource_limits": true,
"restricted_env": true
}
},
"shell-mpc": {
"command": ["python3", "/path/to/shell_mpc.py"],
"description": "Shell PTY access",
"enabled": false,
"sandbox": {
"enabled": false,
"note": "Shell access needs full filesystem"
}
},
"calculator": {
"command": ["node", "/path/to/calculator.js"],
"description": "Math calculations",
"enabled": true,
"sandbox": {
"enabled": true,
"resource_limits": true,
"restricted_env": false,
"note": "Needs NODE_PATH but can have resource limits"
}
}
},
"session": {
"default_timeout": 300,
"max_sessions_per_service": 100,
"cleanup_interval": 60,
"sandbox_dir": "./sandbox"
},
"rate_limiting": {
"default_limits": ["200 per day", "50 per hour"],
"tools_limit": "30 per minute",
"call_limit": "10 per minute",
"session_delete_limit": "20 per minute",
"storage_uri": "memory://"
},
"logging": {
"level": "INFO",
"file": "gateway.log"
}
}The gateway supports environment variable substitution in config.json using the format ${VAR_NAME|default_value}.
Example usage:
Method 1: Using .env file (recommended)
# Copy the example
cp .env.example .env
# Edit with your values
vim .env
# Run - Docker Compose automatically reads .env
./mcp-docker.sh start
# Or for non-Docker
source .env
python3 gateway_service.pyMethod 2: Export environment variables
# Set environment variables
export MCP_PORT=9000
export MCP_AUTH_PASSWORD=mysecret
# Run the gateway
python3 gateway_service.pyMethod 3: Inline variables
# Set variables for just this command
MCP_PORT=9000 MCP_AUTH_PASSWORD=mysecret ./mcp-docker.sh startSupported variables:
MCP_HOST: Server bind address (default: 0.0.0.0)MCP_PORT: Server port (default: 8080)MCP_AUTH_USER: Basic auth username (default: admin)MCP_AUTH_PASSWORD: Basic auth password (default: changeme)MCP_AUTH_TOKEN: Bearer token for API access (default: empty)MCP_SESSION_TIMEOUT: Session timeout in seconds (default: 300)MCP_MAX_SESSIONS: Max sessions per service (default: 100)MCP_CLEANUP_INTERVAL: Session cleanup interval in seconds (default: 60)MCP_LOG_LEVEL: Logging level (default: INFO)MCP_LOG_FILE: Log file path (default: gateway.log)
Each service can have its own sandbox configuration:
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable/disable sandboxing completely |
resource_limits |
true |
Apply CPU, memory, process limits |
restricted_env |
true |
Use minimal environment variables |
working_dir |
Current dir | Working directory for the process |
allowed_paths |
N/A | Future: Path access restrictions |
- High Security (Default)
"sandbox": {
"enabled": true,
"resource_limits": true,
"restricted_env": true
}- Medium Security (For services needing env vars)
"sandbox": {
"enabled": true,
"resource_limits": true,
"restricted_env": false
}- No Sandbox (For trusted services needing full access)
"sandbox": {
"enabled": false
}agent.add_skill("mcp_gateway", {
"gateway_url": "https://localhost:8080",
"auth_user": "admin",
"auth_password": "changeme",
"services": [
{
"name": "todo",
"tools": ["add_todo", "list_todos"] # Specific tools only
},
{
"name": "calculator",
"tools": "*" # All tools
}
],
"session_timeout": 300, # Override default timeout
"tool_prefix": "mcp_", # Prefix for SWAIG function names
"retry_attempts": 3, # Gateway connection retries
"request_timeout": 30, # Individual request timeout
"verify_ssl": True # SSL certificate verification
})Health check endpoint
curl http://localhost:8080/healthList available MCP services
curl -u admin:changeme http://localhost:8080/servicesGet tools for a specific service
curl -u admin:changeme http://localhost:8080/services/todo/toolsCall a tool on a service
Using Basic Auth:
curl -u admin:changeme -X POST http://localhost:8080/services/todo/call \
-H "Content-Type: application/json" \
-d '{
"tool": "add_todo",
"arguments": {"text": "Test item"},
"session_id": "test-123",
"timeout": 300
}'Using Bearer Token:
curl -X POST http://localhost:8080/services/todo/call \
-H "Authorization: Bearer your-token-here" \
-H "Content-Type: application/json" \
-d '{
"tool": "add_todo",
"arguments": {"text": "Test item"},
"session_id": "test-123"
}'List active sessions
curl -u admin:changeme http://localhost:8080/sessionsClose a specific session
curl -u admin:changeme -X DELETE http://localhost:8080/sessions/test-123- Basic Auth: Username/password authentication
- Bearer Token: Alternative token-based authentication
- Dual Support: Can use either Basic Auth or Bearer tokens
- Service name validation (alphanumeric + dash/underscore, max 64 chars)
- Session ID validation (alphanumeric + dot/dash/underscore, max 128 chars)
- Tool name validation (alphanumeric + dash/underscore, max 64 chars)
- Request size limits (10MB max)
Fully configurable through the rate_limiting section in config.json:
"rate_limiting": {
"default_limits": ["200 per day", "50 per hour"],
"tools_limit": "30 per minute",
"call_limit": "10 per minute",
"session_delete_limit": "20 per minute",
"storage_uri": "memory://"
}default_limits: Global rate limits per IP addresstools_limit: Rate limit for/services/*/toolsendpointscall_limit: Rate limit for/services/*/callendpointssession_delete_limit: Rate limit for session deletionstorage_uri: Storage backend for rate limit counters (memory:// or redis://)
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- X-XSS-Protection: 1; mode=block
- Content-Security-Policy: default-src 'none'
- Strict-Transport-Security (HTTPS only)
Configurable per MCP service with three security levels:
-
High Security (Default)
- Process isolation with resource limits
- Restricted environment variables
- CPU time: 300s, Memory: 512MB, Processes: 10
- File size: 10MB max
-
Medium Security
- Resource limits enabled
- Full environment variables
- For services needing PATH, NODE_PATH, etc.
-
No Sandbox
- Disabled sandboxing for trusted services
- Full filesystem and resource access
- HTTPS support with SSL/TLS
- Session isolation between calls
- Automatic session cleanup
- Security event logging
- Dangerous environment variable filtering
# Start the gateway
cd mcp_gateway
python3 gateway_service.py
# Test with curl
./test/test_gateway.sh# Test the agent with MCP skill
swaig-test test/test_agent.py --list-tools
# IMPORTANT: --call-id must come BEFORE --exec for session persistence
swaig-test test/test_agent.py --call-id test-session --exec mcp_todo_add_todo --text "Buy milk"
swaig-test test/test_agent.py --call-id test-session --exec mcp_todo_list_todos
# WRONG: This won't work - --call-id after --exec is treated as function argument
swaig-test test/test_agent.py --exec mcp_todo_add_todo --text "Buy milk" --call-id test-session
# Generate SWML document
swaig-test test/test_agent.py --dump-swml# test/test_agent.py
from signalwire import AgentBase
class TestMCPAgent(AgentBase):
def __init__(self):
super().__init__(name="MCP Test Agent")
self.add_skill("mcp_gateway", {
"gateway_url": "http://localhost:8080",
"auth_user": "admin",
"auth_password": "changeme",
"services": [{"name": "todo"}]
})
if __name__ == "__main__":
agent = TestMCPAgent()
agent.run()cd mcp_gateway
python3 gateway_service.pyThe Docker setup supports three configuration scenarios:
- Runtime Config (highest priority): Mount config.json at runtime
- Build-time Config: Include config.json when building the image
- Default Config: Falls back to sample_config.json
To pre-configure the image at build time:
# Edit your config.json
cp sample_config.json config.json
vim config.json
# Build with config included
./mcp-docker.sh build # Will include config.json in imagePort Configuration: The Docker setup automatically reads the port from your config.json file. If your config specifies port 8100, Docker will expose the service on port 8100.
The mcp-docker.sh script automatically detects the port from config.json. You can also override it using an environment variable:
# Override port at runtime (must match what's in config.json)
MCP_PORT=8100 ./mcp-docker.sh startNote: The port in the MCP_PORT environment variable should match the port configured in your config.json file, as the container internally listens on the configured port.
The easiest way to manage the Docker deployment is using the provided helper script:
cd mcp_gateway
# Show available commands
./mcp-docker.sh help
# Build the Docker image
./mcp-docker.sh build
# Start in foreground (Ctrl+C to stop)
./mcp-docker.sh start
# Start in background
./mcp-docker.sh start -d
# View logs
./mcp-docker.sh logs
./mcp-docker.sh logs -f # Follow logs
# Check status
./mcp-docker.sh status
# Restart the container
./mcp-docker.sh restart
# Stop the container
./mcp-docker.sh stop
# Open shell in running container
./mcp-docker.sh shell
# Clean up (remove container and volumes)
./mcp-docker.sh cleancd mcp_gateway
docker build -t mcp-gateway .
docker run -p 8080:8080 -v $(pwd)/config.json:/app/config.json mcp-gatewaycd mcp_gateway
docker-compose up
docker-compose up -d # Run in background
docker-compose logs -f # Follow logs
docker-compose down # Stop and remove# Generate or place certificates
mkdir -p certs
# Place server.pem in certs/
# Run with HTTPS
python3 gateway_service.py- Session Creation: First tool call creates session with call_id
- Session Persistence: Sessions maintained across multiple tool calls
- Session Cleanup: Automatic cleanup on timeout or hangup hook
- State Isolation: Each session gets separate MCP server instance
- MCP Server Failures: Automatic restart with backoff
- Network Errors: Retry logic with configurable attempts
- Invalid Requests: Clear error messages returned to SWAIG
- Resource Exhaustion: Reject new sessions when at limit
- Connection Pooling: Reuse HTTP connections to gateway
- Lazy Loading: MCP servers started only when needed
- Efficient Cleanup: Background thread for session management
- Response Caching: Optional caching for read-only operations
-
MCP Server Won't Start
- Check command path in config.json
- Verify MCP server is executable
- Check logs for import errors
- Ensure working directory is correct for sandboxed processes
-
Authentication Failures
- Verify credentials match in config and skill
- Check Basic Auth header format
- For Bearer tokens, ensure "Bearer " prefix is included
-
Session Timeouts
- Increase timeout in skill configuration
- Check gateway logs for premature cleanup
- Monitor for stuck MCP processes
-
SSL Certificate Errors
- For self-signed certs, set
verify_ssl: false - Ensure cert path is correct
- For self-signed certs, set
-
Gateway Shutdown Hangs
- Fixed in latest version with improved thread management
- Ensure you're running the updated code
- Check for zombie MCP processes:
ps aux | grep mcp
-
Session Persistence Issues
- Ensure MCP process doesn't die between calls
- Check reader thread isn't creating thread leaks
- Monitor process count with
ps aux | grep todo_mcp
Enable debug logging:
{
"logging": {
"level": "DEBUG",
"file": "gateway.log"
}
}examples/mcp_gateway_demo.py- Agent connecting to MCP servers through themcp_gatewayskill
- WebSocket Support: Real-time bidirectional communication
- Multi-tenant: Separate auth/permissions per tenant
- Metrics/Monitoring: Prometheus endpoints
- Load Balancing: Multiple gateway instances
- Plugin System: Custom transformations/middleware