ADR-014: Multi-Server Support Architecture

Context

Gatekit supports proxying to one or more upstream MCP servers. Real-world usage patterns often require connecting to multiple MCP servers simultaneously:

  1. Specialized Servers: Different servers provide different capabilities (filesystem, GitHub, databases, etc.)
  2. Service Isolation: Separate servers for different security domains or environments
  3. Performance Distribution: Load distribution across multiple server instances
  4. Flexible Deployment: Ability to start with one server and add more servers over time

The architecture provides:

Decision

We implemented a flat list configuration architecture with intelligent request routing:

Core Architecture

  1. ServerManager: Centralized management of multiple upstream server connections
  2. Tool Name Prefixing: Automatic prefixing of tool names with server identifiers
  3. Request Routing: Parse tool names to route requests to appropriate servers
  4. Consistent Architecture: Unified handling whether one or multiple servers are configured

Configuration Design

# Single server (ALL servers must have names for consistency)
upstreams:
  - name: "filesystem"
    command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/"]

# Multiple servers
upstreams:
  - name: "filesystem"
    command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/"]
  - name: "github"
    command: ["npx", "-y", "@modelcontextprotocol/server-github"]

IMPORTANT: All servers MUST have names. This simplifies plugin configuration and provides consistent architecture.

Tool Discovery and Routing

# Tool names are automatically prefixed with double underscore separator
# Original: "read_file" -> Multi-server: "filesystem__read_file"
# Original: "create_issue" -> Multi-server: "github__create_issue"

def _parse_tool_name(self, tool_name: str) -> Tuple[str, str]:
    """Parse server name and original tool name"""
    if '__' in tool_name:
        parts = tool_name.split('__', 1)
        if parts[0] in self.server_manager.servers:
            return parts[0], parts[1]
    return "default", tool_name

Implementation Components

  1. ServerManager Class: Handles lifecycle management of multiple server connections
  2. Enhanced UpstreamServer: Supports named instances and tool prefixing
  3. Request Routing Layer: Intelligent routing based on tool name parsing
  4. Plugin Context Extension: Security plugins receive server context information

Alternatives Considered

Alternative 1: Hierarchical Configuration

servers:
  filesystem:
    type: "mcp"
    config:
      command: ["npx", "-y", "@modelcontextprotocol/server-filesystem"]
  github:
    type: "mcp"
    config:
      command: ["npx", "-y", "@modelcontextprotocol/server-github"]

Rejected: More complex, harder to migrate existing configurations, over-engineered for current needs.

Alternative 2: Route-Based Configuration

routes:
  - pattern: "file_*"
    server: "filesystem"
  - pattern: "github_*"
    server: "github"

Rejected: Requires explicit routing rules, more configuration overhead, less intuitive.

Alternative 3: Namespace-Based Tools

servers:
  - namespace: "fs"
    command: ["npx", "-y", "@modelcontextprotocol/server-filesystem"]

Rejected: Requires client-side namespace awareness, breaks MCP protocol transparency.

Implementation Strategy

Phase 1: Infrastructure (Completed)

Phase 2: Core Functionality (Completed)

Phase 3: Production Features (Completed)

Migration Strategy

Zero-Breaking-Change Migration:

  1. Existing upstream_server configurations continue to work unchanged
  2. New upstream_servers provides multi-server capabilities
  3. Configuration validation prevents mixing both approaches
  4. All tool names are namespaced consistently (unified multi-server architecture)

Example migration:

# Before (continues to work)
upstream_server:
  command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/"]

# After (new capability)
upstream_servers:
  - name: "filesystem"
    command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/"]

Security Implications

Enhanced Security Context

Security Benefits

Performance Considerations

Optimization Features

Performance Impact

Testing Strategy

Comprehensive Coverage

Test Scenarios

Configuration Schema

class UpstreamConfig(BaseModel):
    """Configuration for a single upstream server"""
    name: str = Field(..., description="Unique server identifier")
    command: List[str] = Field(..., description="Command and arguments to start the server")
    # Note: Environment variables are NOT configurable per-server.
    # Set environment variables in your MCP client or shell instead.
    
class GatewayConfig(BaseModel):
    """Updated gateway configuration"""
    upstream_server: Optional[ServerConfigLegacy] = None  # Backward compatibility
    upstream_servers: Optional[List[ServerConfig]] = None  # New multi-server
    
    @validator('upstream_servers')
    def validate_server_config(cls, v, values):
        if v and values.get('upstream_server'):
            raise ValueError("Cannot specify both upstream_server and upstream_servers")
        return v

Consequences

Positive

Negative

Neutral

Decision Rationale

The flat list configuration with tool name prefixing was chosen because:

  1. Simplicity: Straightforward configuration and mental model
  2. Compatibility: Zero breaking changes for existing users
  3. Transparency: Works with any MCP client without modifications
  4. Scalability: Efficient resource management and concurrent operations
  5. Security: Maintains Gatekit's security-first approach
  6. Maintainability: Clean code architecture that extends existing patterns

This approach provides a solid foundation for multi-server support while preserving the simplicity and reliability that are core to Gatekit's design philosophy.

Future Enhancements

  1. Dynamic Server Management: Runtime addition/removal of servers
  2. Load Balancing: Distribute requests across multiple instances of the same server
  3. Health Monitoring: Advanced health checks and automatic failover
  4. Configuration Templating: Simplified configuration for common patterns
  5. Tool Conflict Resolution: Strategies for handling duplicate tool names