MCP to SWAIG Gateway 🔗 ↑ TOC

Overview 🔗 ↑ TOC

The MCP-SWAIG Gateway bridges Model Context Protocol (MCP) servers with SignalWire AI Gateway (SWAIG) functions, allowing SignalWire AI agents to seamlessly interact with MCP-based tools. This gateway acts as a translation layer and session manager between the two protocols.

Architecture 🔗 ↑ TOC

Components 🔗 ↑ TOC

  1. MCP Gateway Service (mcp_gateway/)
  2. HTTP/HTTPS server with Basic Authentication
  3. Manages multiple MCP server instances
  4. Handles session lifecycle per SignalWire call
  5. Translates between SWAIG and MCP protocols

  6. MCP Gateway Skill (signalwire_agents/skills/mcp_gateway/)

  7. SignalWire skill that connects agents to the gateway
  8. Dynamically creates SWAIG functions from MCP tools
  9. Manages session lifecycle using call_id

  10. Test MCP Server (mcp_gateway/test/todo_mcp.py)

  11. Simple todo list MCP server for testing
  12. Demonstrates stateful MCP server implementation

Protocol Flow 🔗 ↑ TOC

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---->|

Message Envelope Format 🔗 ↑ TOC

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"
    }
}

Directory Structure 🔗 ↑ TOC

mcp_gateway/
├── config.json              # Gateway configuration
├── gateway_service.py       # Main HTTP/HTTPS server
├── mcp_manager.py          # MCP server lifecycle management
├── session_manager.py      # Session handling and timeouts
├── requirements.txt        # Python dependencies
├── 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/
    ├── config.example.json
    └── generate_cert.sh   # Generate self-signed certificate

signalwire_agents/skills/mcp_gateway/
├── __init__.py
├── skill.py              # MCP gateway skill
└── README.md            # Skill documentation

Configuration 🔗 ↑ TOC

Gateway Configuration (config.json) 🔗 ↑ TOC

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"
    }
}

Environment Variable Substitution 🔗 ↑ TOC

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.py

Method 2: Export environment variables

# Set environment variables
export MCP_PORT=9000
export MCP_AUTH_PASSWORD=mysecret

# Run the gateway
python3 gateway_service.py

Method 3: Inline variables

# Set variables for just this command
MCP_PORT=9000 MCP_AUTH_PASSWORD=mysecret ./mcp-docker.sh start

Supported variables:

Sandbox Configuration Options 🔗 ↑ TOC

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

Sandbox Profiles 🔗 ↑ TOC

  1. High Security (Default)
"sandbox": {
    "enabled": true,
    "resource_limits": true,
    "restricted_env": true
}
  1. Medium Security (For services needing env vars)
"sandbox": {
    "enabled": true,
    "resource_limits": true,
    "restricted_env": false
}
  1. No Sandbox (For trusted services needing full access)
"sandbox": {
    "enabled": false
}

Skill Configuration 🔗 ↑ TOC

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
})

API Endpoints 🔗 ↑ TOC

Gateway Service Endpoints 🔗 ↑ TOC

GET /health 🔗 ↑ TOC

Health check endpoint

curl http://localhost:8080/health

GET /services 🔗 ↑ TOC

List available MCP services

curl -u admin:changeme http://localhost:8080/services

GET /services/{service_name}/tools 🔗 ↑ TOC

Get tools for a specific service

curl -u admin:changeme http://localhost:8080/services/todo/tools

POST /services/{service_name}/call 🔗 ↑ TOC

Call 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"
  }'

GET /sessions 🔗 ↑ TOC

List active sessions

curl -u admin:changeme http://localhost:8080/sessions

DELETE /sessions/{session_id} 🔗 ↑ TOC

Close a specific session

curl -u admin:changeme -X DELETE http://localhost:8080/sessions/test-123

Security Features 🔗 ↑ TOC

Authentication 🔗 ↑ TOC

Input Validation 🔗 ↑ TOC

Rate Limiting 🔗 ↑ TOC

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://"
}

Security Headers 🔗 ↑ TOC

Process Sandboxing 🔗 ↑ TOC

Configurable per MCP service with three security levels:

  1. High Security (Default)
  2. Process isolation with resource limits
  3. Restricted environment variables
  4. CPU time: 300s, Memory: 512MB, Processes: 10
  5. File size: 10MB max

  6. Medium Security

  7. Resource limits enabled
  8. Full environment variables
  9. For services needing PATH, NODE_PATH, etc.

  10. No Sandbox

  11. Disabled sandboxing for trusted services
  12. Full filesystem and resource access

Other Security Features 🔗 ↑ TOC

Testing 🔗 ↑ TOC

1. Unit Testing the Gateway 🔗 ↑ TOC

# Start the gateway
cd mcp_gateway
python3 gateway_service.py

# Test with curl
./test/test_gateway.sh

2. Testing with SWAIG CLI 🔗 ↑ TOC

# 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

3. End-to-End Testing 🔗 ↑ TOC

# test/test_agent.py
from signalwire_agents 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()

Deployment 🔗 ↑ TOC

Local Development 🔗 ↑ TOC

cd mcp_gateway
python3 gateway_service.py

Docker Deployment 🔗 ↑ TOC

Configuration Options 🔗 ↑ TOC

The Docker setup supports three configuration scenarios:

  1. Runtime Config (highest priority): Mount config.json at runtime
  2. Build-time Config: Include config.json when building the image
  3. 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 image

Port 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 start

Note: 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.

Using mcp-docker.sh Helper Script 🔗 ↑ TOC

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 clean

Manual Docker Commands 🔗 ↑ TOC

cd mcp_gateway
docker build -t mcp-gateway .
docker run -p 8080:8080 -v $(pwd)/config.json:/app/config.json mcp-gateway

Docker Compose 🔗 ↑ TOC

cd mcp_gateway
docker-compose up
docker-compose up -d  # Run in background
docker-compose logs -f  # Follow logs
docker-compose down  # Stop and remove

Production with HTTPS 🔗 ↑ TOC

# Generate or place certificates
mkdir -p certs
# Place server.pem in certs/

# Run with HTTPS
python3 gateway_service.py

Implementation Details 🔗 ↑ TOC

Session Management 🔗 ↑ TOC

  1. Session Creation: First tool call creates session with call_id
  2. Session Persistence: Sessions maintained across multiple tool calls
  3. Session Cleanup: Automatic cleanup on timeout or hangup hook
  4. State Isolation: Each session gets separate MCP server instance

Error Handling 🔗 ↑ TOC

  1. MCP Server Failures: Automatic restart with backoff
  2. Network Errors: Retry logic with configurable attempts
  3. Invalid Requests: Clear error messages returned to SWAIG
  4. Resource Exhaustion: Reject new sessions when at limit

Performance Optimization 🔗 ↑ TOC

  1. Connection Pooling: Reuse HTTP connections to gateway
  2. Lazy Loading: MCP servers started only when needed
  3. Efficient Cleanup: Background thread for session management
  4. Response Caching: Optional caching for read-only operations

Troubleshooting 🔗 ↑ TOC

Common Issues 🔗 ↑ TOC

  1. MCP Server Won't Start
  2. Check command path in config.json
  3. Verify MCP server is executable
  4. Check logs for import errors
  5. Ensure working directory is correct for sandboxed processes

  6. Authentication Failures

  7. Verify credentials match in config and skill
  8. Check Basic Auth header format
  9. For Bearer tokens, ensure "Bearer " prefix is included

  10. Session Timeouts

  11. Increase timeout in skill configuration
  12. Check gateway logs for premature cleanup
  13. Monitor for stuck MCP processes

  14. SSL Certificate Errors

  15. For self-signed certs, set verify_ssl: false
  16. Ensure cert path is correct

  17. Gateway Shutdown Hangs

  18. Fixed in latest version with improved thread management
  19. Ensure you're running the updated code
  20. Check for zombie MCP processes: ps aux | grep mcp

  21. Session Persistence Issues

  22. Ensure MCP process doesn't die between calls
  23. Check reader thread isn't creating thread leaks
  24. Monitor process count with ps aux | grep todo_mcp

Debug Mode 🔗 ↑ TOC

Enable debug logging:

{
    "logging": {
        "level": "DEBUG",
        "file": "gateway.log"
    }
}

Future Enhancements 🔗 ↑ TOC

  1. WebSocket Support: Real-time bidirectional communication
  2. Multi-tenant: Separate auth/permissions per tenant
  3. Metrics/Monitoring: Prometheus endpoints
  4. Load Balancing: Multiple gateway instances
  5. Plugin System: Custom transformations/middleware