Skip to content

[CHORE]: End-to-End MCP Gateway Stack Testing Harness (mcpgateway, translate, wrapper, mcp-servers)Β #312

@crivetimihai

Description

@crivetimihai

🧭 CHORE Summary β€” End-to-End MCP Gateway Stack Testing Harness

Implement a comprehensive end-to-end testing system that validates the complete MCP Gateway stack with full component integration across all transport protocols and data stores:

  1. Multi-protocol fast-time-server deployment (stdio, SSE, streamable HTTP)
  2. Protocol translation matrix testing with mcpgateway.translate (any-to-any conversion)
  3. Wrapper authentication flow validation (stdio β†’ SSE with OAuth2 Bearer)
  4. Full gateway stack with Redis cache and PostgreSQL persistence
  5. GitHub Actions integration with matrix testing across all component combinations
  6. Comprehensive flow validation from client request to data persistence and federation

πŸ—οΈ Architecture Overview

Complete Stack Architecture

graph TB
    %% Client Layer
    subgraph "Client Applications"
        MCP_CLIENT[MCP Inspector/Claude Desktop<br/>JSON-RPC stdio]
        CURL_CLIENT[cURL/HTTP Client<br/>REST API]
        SSE_CLIENT[Browser/SSE Client<br/>EventSource]
    end

    %% Wrapper Layer
    subgraph "Protocol Wrapper Layer"
        WRAPPER[mcpgateway.wrapper<br/>stdio ↔ SSE Bridge<br/>OAuth2 Authentication]
    end

    %% Translation Layer
    subgraph "Protocol Translation Layer"
        TRANSLATE_STDIO[mcpgateway.translate<br/>stdio β†’ SSE]
        TRANSLATE_SSE[mcpgateway.translate<br/>SSE β†’ stdio]
        TRANSLATE_HTTP[mcpgateway.translate<br/>HTTP β†’ SSE]
    end

    %% Gateway Core
    subgraph "MCP Gateway Core"
        GATEWAY[mcpgateway<br/>Main Gateway Instance<br/>FastAPI + L1 Cache]
        ADMIN_UI[Admin UI<br/>HTMX + Alpine.js]
    end

    %% Backend Services
    subgraph "Backend MCP Servers"
        FAST_TIME_STDIO[fast-time-server<br/>stdio mode]
        FAST_TIME_SSE[fast-time-server<br/>SSE mode :8001]
        FAST_TIME_HTTP[fast-time-server<br/>streamable HTTP :8002]
        EXTERNAL_MCP[External MCP Servers<br/>Various Protocols]
    end

    %% Data Layer
    subgraph "Data Persistence Layer"
        REDIS[(Redis Cluster<br/>L2 Cache + Sessions)]
        POSTGRES[(PostgreSQL<br/>Primary Storage)]
    end

    %% Federation Layer
    subgraph "Federation Layer"
        PEER_GATEWAY_1[Peer Gateway 1<br/>Remote Instance]
        PEER_GATEWAY_2[Peer Gateway 2<br/>Remote Instance]
        FEDERATION_REGISTRY[Federation Registry<br/>mDNS + Manual Config]
    end

    %% Connections - Client to Wrapper
    MCP_CLIENT -->|JSON-RPC stdio| WRAPPER
    CURL_CLIENT -->|HTTP/REST| GATEWAY
    SSE_CLIENT -->|EventSource| GATEWAY

    %% Wrapper to Gateway
    WRAPPER -->|Authenticated SSE| GATEWAY

    %% Translation flows
    TRANSLATE_STDIO -->|Expose stdio as SSE| GATEWAY
    TRANSLATE_SSE -->|Bridge SSE to stdio| FAST_TIME_STDIO
    TRANSLATE_HTTP -->|Convert HTTP to SSE| GATEWAY

    %% Gateway to backends
    GATEWAY -->|stdio subprocess| FAST_TIME_STDIO
    GATEWAY -->|HTTP SSE| FAST_TIME_SSE
    GATEWAY -->|HTTP REST| FAST_TIME_HTTP
    GATEWAY -->|Various protocols| EXTERNAL_MCP

    %% Gateway to data layer
    GATEWAY -->|Cache operations| REDIS
    GATEWAY -->|CRUD operations| POSTGRES

    %% Federation connections
    GATEWAY <-->|Federation protocol| PEER_GATEWAY_1
    GATEWAY <-->|Federation protocol| PEER_GATEWAY_2
    GATEWAY -->|Service discovery| FEDERATION_REGISTRY

    %% Admin UI
    ADMIN_UI -.->|Management| GATEWAY

    classDef client fill:#e3f2fd
    classDef wrapper fill:#f3e5f5
    classDef translate fill:#e8f5e8
    classDef gateway fill:#fff3e0
    classDef backend fill:#fce4ec
    classDef data fill:#f1f8e9
    classDef federation fill:#e0f2f1

    class MCP_CLIENT,CURL_CLIENT,SSE_CLIENT client
    class WRAPPER wrapper
    class TRANSLATE_STDIO,TRANSLATE_SSE,TRANSLATE_HTTP translate
    class GATEWAY,ADMIN_UI gateway
    class FAST_TIME_STDIO,FAST_TIME_SSE,FAST_TIME_HTTP,EXTERNAL_MCP backend
    class REDIS,POSTGRES data
    class PEER_GATEWAY_1,PEER_GATEWAY_2,FEDERATION_REGISTRY federation
Loading

End-to-End Test Flow

sequenceDiagram
    participant Client as MCP Client
    participant Wrapper as mcpgateway.wrapper
    participant Gateway as MCP Gateway
    participant Translate as mcpgateway.translate
    participant TimeServer as fast-time-server
    participant Redis as Redis Cache
    participant Postgres as PostgreSQL

    Note over Client,Postgres: E2E Test Flow: Get Current Time

    %% 1. Client initialization
    Client->>Wrapper: JSON-RPC initialize
    Wrapper->>Gateway: SSE connect (OAuth2 Bearer)
    Gateway->>Redis: Check session cache
    Redis-->>Gateway: Session state
    Gateway-->>Wrapper: SSE connection established
    Wrapper-->>Client: Initialize response

    %% 2. Tool discovery
    Client->>Wrapper: tools/list request
    Wrapper->>Gateway: Forward via SSE
    Gateway->>Postgres: Query registered tools
    Postgres-->>Gateway: Tool definitions
    Gateway->>Redis: Cache tool list
    Gateway-->>Wrapper: Tools response via SSE
    Wrapper-->>Client: Tools list

    %% 3. Tool execution - stdio path
    Client->>Wrapper: tools/call get_current_time
    Wrapper->>Gateway: Forward via SSE
    Gateway->>Translate: Route to stdio server
    Translate->>TimeServer: JSON-RPC stdio
    TimeServer-->>Translate: Time response
    Translate-->>Gateway: SSE response
    Gateway->>Postgres: Log tool call
    Gateway->>Redis: Cache result
    Gateway-->>Wrapper: Tool result via SSE
    Wrapper-->>Client: Time result

    %% 4. Tool execution - HTTP path (parallel test)
    Client->>Gateway: Direct HTTP call
    Gateway->>TimeServer: HTTP streamable request
    TimeServer-->>Gateway: HTTP time response
    Gateway->>Postgres: Log tool call
    Gateway->>Redis: Cache result
    Gateway-->>Client: JSON response

    %% 5. Federation test
    Gateway->>Gateway: Discover federated tools
    Gateway->>Redis: Check federation cache
    Gateway->>Postgres: Store federation state
Loading

🌊 Test Matrix & Component Combinations

Test Suite fast-time-server Translation Gateway Wrapper Data Stores Duration
Minimal stdio only None Memory only Basic SQLite 5 min
Basic stdio + SSE stdio→SSE Redis L2 OAuth2 Redis + SQLite 10 min
Standard All 3 protocols Full matrix Redis + L1 Full auth Redis + PostgreSQL 20 min
Enterprise All + federation All + mocks Cluster mode Multi-tenant Redis cluster + PostgreSQL HA 45 min

Protocol Combinations Tested: 9 combinations (3 time-server modes Γ— 3 client access patterns)


🧱 Areas Affected

  • Make targets β€” make e2e-test-{suite}, make test-protocol-matrix, make test-full-stack
  • CI / GitHub Actions β€” Matrix testing across all component combinations
  • Docker Compose β€” Multi-service stack with all components
  • Test infrastructure β€” Protocol-specific test clients, component health checks
  • Integration tests β€” Complete flow validation from client to persistence
  • Documentation β€” Comprehensive E2E testing guide with troubleshooting

βš™οΈ Context / Rationale

Current testing gaps that prevent confident production deployment:

Critical Question Today After This Epic
Full stack integration works end-to-end? ❓ Unknown βœ… All component combinations tested
Protocol translation handles edge cases? ❓ Unknown πŸ“Š Matrix testing across all protocol pairs
Authentication flow secure in wrapper chain? ❓ Unknown πŸ” OAuth2 Bearer token validation
Data persistence consistent across flows? ❓ Unknown πŸ“‹ Database state validation
Federation works with real deployments? ❓ Unknown 🌐 Multi-gateway federation testing

Real-world scenarios validated:

  • Complete MCP client workflow through wrapper with authentication
  • Protocol translation across stdio ↔ SSE ↔ HTTP patterns
  • Cache consistency between L1 (in-memory) and L2 (Redis)
  • Database persistence of tools, sessions, and federation state
  • Error handling and graceful degradation across component failures

πŸ“ Enhanced Test Environment Architecture

Complete E2E Test Stack:

flowchart TD
    %% Test Orchestration
    subgraph "Test Orchestration Layer"
        PYTEST[pytest E2E Suite<br/>Test Discovery & Execution]
        DOCKER_COMPOSE[Docker Compose<br/>Stack Orchestration]
        HEALTH_CHECK[Health Check Service<br/>Component Readiness]
    end

    %% Test Clients
    subgraph "Test Client Layer"
        MCP_TEST_CLIENT[MCP Test Client<br/>JSON-RPC stdio]
        HTTP_TEST_CLIENT[HTTP Test Client<br/>REST + SSE]
        FEDERATION_CLIENT[Federation Test Client<br/>Multi-gateway]
    end

    %% Component Under Test
    subgraph "Components Under Test"
        WRAPPER_CUT[mcpgateway.wrapper<br/>stdio ↔ SSE Bridge]
        TRANSLATE_CUT[mcpgateway.translate<br/>Protocol Matrix]
        GATEWAY_CUT[mcpgateway<br/>Main Gateway]
        TIME_SERVER_CUT[fast-time-server<br/>All 3 Modes]
    end

    %% Data Layer
    subgraph "Data Services"
        REDIS_TEST[Redis<br/>Cache + Sessions]
        POSTGRES_TEST[PostgreSQL<br/>Persistence]
        SQLITE_TEST[SQLite<br/>Lightweight option]
    end

    %% Mock Services
    subgraph "Mock & Federation Services"
        MOCK_MCP[Mock MCP Servers<br/>Protocol testing]
        PEER_GATEWAY_MOCK[Mock Peer Gateways<br/>Federation testing]
        AUTH_MOCK[Mock Auth Service<br/>OAuth2 testing]
    end

    %% Test Flow Connections
    PYTEST --> DOCKER_COMPOSE
    DOCKER_COMPOSE --> HEALTH_CHECK
    
    MCP_TEST_CLIENT --> WRAPPER_CUT
    HTTP_TEST_CLIENT --> GATEWAY_CUT
    FEDERATION_CLIENT --> GATEWAY_CUT
    
    WRAPPER_CUT --> GATEWAY_CUT
    TRANSLATE_CUT --> GATEWAY_CUT
    GATEWAY_CUT --> TIME_SERVER_CUT
    
    GATEWAY_CUT --> REDIS_TEST
    GATEWAY_CUT --> POSTGRES_TEST
    GATEWAY_CUT --> SQLITE_TEST
    
    GATEWAY_CUT --> MOCK_MCP
    GATEWAY_CUT --> PEER_GATEWAY_MOCK
    WRAPPER_CUT --> AUTH_MOCK

    classDef orchestration fill:#ffeb3b
    classDef client fill:#4caf50
    classDef component fill:#2196f3
    classDef data fill:#ff9800
    classDef mock fill:#9c27b0

    class PYTEST,DOCKER_COMPOSE,HEALTH_CHECK orchestration
    class MCP_TEST_CLIENT,HTTP_TEST_CLIENT,FEDERATION_CLIENT client
    class WRAPPER_CUT,TRANSLATE_CUT,GATEWAY_CUT,TIME_SERVER_CUT component
    class REDIS_TEST,POSTGRES_TEST,SQLITE_TEST data
    class MOCK_MCP,PEER_GATEWAY_MOCK,AUTH_MOCK mock
Loading

πŸ“‹ Enhanced Acceptance Criteria

# Criteria Validation Method
1 Complete stack integration: All components work together end-to-end E2E test suite passes with 100% component coverage
2 Protocol matrix testing: All 9 protocol combinations validated Matrix test reports show successful translation paths
3 Authentication security: OAuth2 Bearer flow secure in production Security test suite validates token handling
4 Data consistency: All persistence layers maintain state correctly Database validation tests pass
5 Federation functionality: Multi-gateway federation works reliably Federation test suite with mock gateways
6 CI integration: All tests run automatically in GitHub Actions CI pipeline completes successfully

πŸ› οΈ Comprehensive Task List

Phase 1: Fast-Time-Server Multi-Protocol Deployment

  • 1.1 Multi-protocol time server configuration docker-compose.e2e.yml

    # Fast-time-server in all three modes
    fast-time-stdio:
      build:
        context: ./tests/fixtures/fast-time-server
        dockerfile: Dockerfile
      command: ["python", "-m", "fast_time_server", "--mode", "stdio"]
      stdin_open: true
      tty: true
      networks:
        - e2e-test-network
    
    fast-time-sse:
      build:
        context: ./tests/fixtures/fast-time-server
        dockerfile: Dockerfile
      command: ["python", "-m", "fast_time_server", "--mode", "sse", "--port", "8001"]
      ports:
        - "8001:8001"
      networks:
        - e2e-test-network
      healthcheck:
        test: ["CMD", "curl", "-f", "http://localhost:8001/health"]
        interval: 5s
        timeout: 3s
        retries: 5
    
    fast-time-http:
      build:
        context: ./tests/fixtures/fast-time-server
        dockerfile: Dockerfile
      command: ["python", "-m", "fast_time_server", "--mode", "streamable-http", "--port", "8002"]
      ports:
        - "8002:8002"
      networks:
        - e2e-test-network
      healthcheck:
        test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
        interval: 5s
        timeout: 3s
        retries: 5
  • 1.2 Enhanced fast-time-server implementation tests/fixtures/fast-time-server/fast_time_server.py

    # Enhanced fast-time-server supporting all 3 protocols
    import asyncio
    import json
    import sys
    from datetime import datetime
    from typing import Any, Dict, Optional
    import argparse
    from fastapi import FastAPI, Request
    from fastapi.responses import StreamingResponse
    from sse_starlette.sse import EventSourceResponse
    import uvicorn
    
    class FastTimeServer:
        def __init__(self, mode: str = "stdio"):
            self.mode = mode
            self.app = FastAPI() if mode in ["sse", "streamable-http"] else None
            self._setup_handlers()
    
        def _setup_handlers(self):
            """Setup protocol-specific handlers"""
            if self.mode == "stdio":
                self._setup_stdio_handlers()
            elif self.mode == "sse":
                self._setup_sse_handlers()
            elif self.mode == "streamable-http":
                self._setup_http_handlers()
    
        def _setup_stdio_handlers(self):
            """Setup stdio JSON-RPC handlers"""
            # Standard MCP stdio implementation
            pass
    
        def _setup_sse_handlers(self):
            """Setup SSE endpoint handlers"""
            @self.app.get("/sse")
            async def sse_endpoint():
                async def event_generator():
                    while True:
                        yield {
                            "event": "keepalive",
                            "data": json.dumps({"timestamp": datetime.utcnow().isoformat()})
                        }
                        await asyncio.sleep(30)
                return EventSourceResponse(event_generator())
    
            @self.app.post("/message")
            async def message_handler(request: Request):
                data = await request.json()
                if data.get("method") == "tools/call" and data.get("params", {}).get("name") == "get_current_time":
                    return self._get_time_response(data.get("id"))
                return {"jsonrpc": "2.0", "id": data.get("id"), "error": {"code": -32601, "message": "Method not found"}}
    
        def _setup_http_handlers(self):
            """Setup streamable HTTP handlers"""
            @self.app.get("/health")
            async def health_check():
                return {"status": "healthy", "mode": self.mode}
    
            @self.app.post("/tools/call")
            async def call_tool(request: Request):
                data = await request.json()
                if data.get("name") == "get_current_time":
                    return self._get_time_response()
                return {"error": "Tool not found"}
    
        def _get_time_response(self, request_id: Optional[str] = None) -> Dict[str, Any]:
            """Generate time response in MCP format"""
            current_time = datetime.utcnow().isoformat() + "Z"
            
            response = {
                "content": [{
                    "type": "text",
                    "text": json.dumps({
                        "current_time": current_time,
                        "timezone": "UTC",
                        "mode": self.mode
                    })
                }],
                "isError": False
            }
            
            if request_id:
                return {"jsonrpc": "2.0", "id": request_id, "result": response}
            return response
    
        async def run_stdio(self):
            """Run in stdio mode"""
            while True:
                try:
                    line = await asyncio.get_event_loop().run_in_executor(None, sys.stdin.readline)
                    if not line:
                        break
                    
                    data = json.loads(line.strip())
                    response = await self._handle_stdio_request(data)
                    print(json.dumps(response), flush=True)
                except Exception as e:
                    error_response = {
                        "jsonrpc": "2.0",
                        "id": data.get("id") if 'data' in locals() else None,
                        "error": {"code": -32603, "message": f"Internal error: {str(e)}"}
                    }
                    print(json.dumps(error_response), flush=True)
    
        async def _handle_stdio_request(self, data: Dict[str, Any]) -> Dict[str, Any]:
            """Handle stdio JSON-RPC request"""
            method = data.get("method")
            
            if method == "initialize":
                return {
                    "jsonrpc": "2.0",
                    "id": data.get("id"),
                    "result": {
                        "protocolVersion": "2025-03-26",
                        "capabilities": {"tools": {"listChanged": False}},
                        "serverInfo": {"name": "fast-time-server", "version": "1.0.0"}
                    }
                }
            elif method == "tools/list":
                return {
                    "jsonrpc": "2.0",
                    "id": data.get("id"),
                    "result": {
                        "tools": [{
                            "name": "get_current_time",
                            "description": "Get the current time",
                            "inputSchema": {
                                "type": "object",
                                "properties": {
                                    "timezone": {"type": "string", "default": "UTC"}
                                }
                            }
                        }]
                    }
                }
            elif method == "tools/call":
                return {
                    "jsonrpc": "2.0",
                    "id": data.get("id"),
                    "result": self._get_time_response()
                }
            else:
                return {
                    "jsonrpc": "2.0",
                    "id": data.get("id"),
                    "error": {"code": -32601, "message": "Method not found"}
                }
    
        def run(self, host: str = "0.0.0.0", port: int = 8000):
            """Run the server"""
            if self.mode == "stdio":
                asyncio.run(self.run_stdio())
            else:
                uvicorn.run(self.app, host=host, port=port)
    
    if __name__ == "__main__":
        parser = argparse.ArgumentParser()
        parser.add_argument("--mode", choices=["stdio", "sse", "streamable-http"], default="stdio")
        parser.add_argument("--port", type=int, default=8000)
        args = parser.parse_args()
        
        server = FastTimeServer(args.mode)
        server.run(port=args.port)

Phase 2: Protocol Translation Matrix Testing

  • 2.1 Translation service configuration docker-compose.e2e.yml (addition)

    # Protocol translation services
    translate-stdio-to-sse:
      build:
        context: .
        dockerfile: Dockerfile
      command: ["python", "-m", "mcpgateway.translate", "--stdio", "python /app/tests/fixtures/fast-time-server/fast_time_server.py --mode stdio", "--port", "9001"]
      ports:
        - "9001:9001"
      depends_on:
        - fast-time-stdio
      networks:
        - e2e-test-network
    
    translate-sse-to-stdio:
      build:
        context: .
        dockerfile: Dockerfile
      command: ["python", "-m", "mcpgateway.translate", "--sse", "http://fast-time-sse:8001/sse"]
      stdin_open: true
      tty: true
      depends_on:
        - fast-time-sse
      networks:
        - e2e-test-network
  • 2.2 Protocol matrix test suite tests/e2e/test_protocol_matrix.py

    # Comprehensive protocol translation testing
    import pytest
    import asyncio
    import json
    import httpx
    from typing import Dict, Any
    from tests.e2e.fixtures import E2ETestHarness
    
    class TestProtocolMatrix:
        """Test all protocol translation combinations"""
    
        @pytest.fixture
        def test_harness(self):
            return E2ETestHarness()
    
        @pytest.mark.asyncio
        async def test_stdio_to_sse_translation(self, test_harness):
            """Test stdio server exposed via SSE"""
            # Connect to translated SSE endpoint
            async with httpx.AsyncClient() as client:
                # Test SSE connection
                response = await client.get("http://localhost:9001/sse")
                assert response.status_code == 200
                
                # Test message sending
                message = {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "tools/call",
                    "params": {"name": "get_current_time"}
                }
                
                response = await client.post("http://localhost:9001/message", json=message)
                assert response.status_code == 200
                
                result = response.json()
                assert result["jsonrpc"] == "2.0"
                assert "result" in result
    
        @pytest.mark.asyncio  
        async def test_sse_to_stdio_translation(self, test_harness):
            """Test SSE server accessed via stdio"""
            # This would test the reverse translation
            # Implementation would use subprocess to communicate with translate service
            pass
    
        @pytest.mark.asyncio
        async def test_full_translation_chain(self, test_harness):
            """Test complete chain: stdio β†’ SSE β†’ HTTP β†’ gateway"""
            # Test complex translation chains
            pass
    
        @pytest.mark.parametrize("source_protocol,target_protocol", [
            ("stdio", "sse"),
            ("sse", "stdio"),
            ("http", "sse"),
            ("sse", "http")
        ])
        async def test_protocol_translation_matrix(self, source_protocol, target_protocol, test_harness):
            """Test all protocol translation combinations"""
            result = await test_harness.test_protocol_translation(source_protocol, target_protocol)
            assert result.success
            assert result.response_time < 1.0  # Max 1 second
            assert result.data_integrity_check_passed

Phase 3: Wrapper Authentication Flow Testing

  • 3.1 Wrapper service with authentication docker-compose.e2e.yml (addition)

    # MCP Gateway Wrapper with OAuth2
    mcpgateway-wrapper:
      build:
        context: .
        dockerfile: Dockerfile
      command: ["python", "-m", "mcpgateway.wrapper"]
      environment:
        - MCP_AUTH_TOKEN=${E2E_TEST_TOKEN}
        - MCP_SERVER_CATALOG_URLS=http://mcpgateway:4444/servers/1
        - LOG_LEVEL=debug
      stdin_open: true
      tty: true
      depends_on:
        - mcpgateway
      networks:
        - e2e-test-network
  • 3.2 Authentication flow tests tests/e2e/test_auth_flow.py

    # Authentication and security testing
    import pytest
    import asyncio
    import json
    import subprocess
    from tests.e2e.fixtures import E2ETestHarness
    
    class TestAuthenticationFlow:
        """Test complete authentication flow through wrapper"""
    
        @pytest.fixture
        def test_harness(self):
            return E2ETestHarness()
    
        @pytest.mark.asyncio
        async def test_wrapper_oauth2_flow(self, test_harness):
            """Test OAuth2 Bearer token authentication through wrapper"""
            # Generate test token
            token = await test_harness.generate_test_token(username="e2e-test", exp=3600)
            
            # Configure wrapper with token
            wrapper_process = await test_harness.start_wrapper_with_token(token)
            
            try:
                # Test initialization through wrapper
                init_request = {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "initialize",
                    "params": {
                        "protocolVersion": "2025-03-26",
                        "capabilities": {},
                        "clientInfo": {"name": "e2e-test", "version": "1.0.0"}
                    }
                }
                
                response = await test_harness.send_stdio_message(wrapper_process, init_request)
                assert response["jsonrpc"] == "2.0"
                assert "result" in response
                assert response["result"]["serverInfo"]["name"] == "mcpgateway-wrapper"
                
                # Test tools listing
                tools_request = {"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
                response = await test_harness.send_stdio_message(wrapper_process, tools_request)
                assert "result" in response
                assert "tools" in response["result"]
                
            finally:
                await test_harness.stop_wrapper(wrapper_process)
    
        @pytest.mark.asyncio
        async def test_wrapper_auth_failure(self, test_harness):
            """Test authentication failure handling"""
            # Test with invalid token
            invalid_token = "invalid.token.here"
            
            wrapper_process = await test_harness.start_wrapper_with_token(invalid_token)
            
            try:
                init_request = {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "initialize",
                    "params": {"protocolVersion": "2025-03-26", "capabilities": {}}
                }
                
                response = await test_harness.send_stdio_message(wrapper_process, init_request)
                assert "error" in response
                assert response["error"]["code"] == -32603  # Internal error
                
            finally:
                await test_harness.stop_wrapper(wrapper_process)

Phase 4: Full Gateway Stack Integration

  • 4.1 Complete stack configuration docker-compose.e2e.yml (addition)

    # Complete MCP Gateway stack
    redis:
      image: redis:7-alpine
      ports:
        - "6379:6379"
      command: redis-server --appendonly yes
      volumes:
        - redis-data:/data
      networks:
        - e2e-test-network
    
    postgres:
      image: postgres:15-alpine
      ports:
        - "5432:5432"
      environment:
        - POSTGRES_DB=mcpgateway_e2e
        - POSTGRES_USER=test
        - POSTGRES_PASSWORD=test
      volumes:
        - postgres-data:/var/lib/postgresql/data
      networks:
        - e2e-test-network
      healthcheck:
        test: ["CMD-SHELL", "pg_isready -U test -d mcpgateway_e2e"]
        interval: 5s
        timeout: 3s
        retries: 5
    
    mcpgateway:
      build:
        context: .
        dockerfile: Dockerfile
      ports:
        - "4444:4444"
      environment:
        - DATABASE_URL=postgresql://test:test@postgres:5432/mcpgateway_e2e
        - REDIS_URL=redis://redis:6379
        - JWT_SECRET_KEY=e2e-test-secret-key
        - BASIC_AUTH_PASSWORD=e2e-test
        - LOG_LEVEL=debug
        - CACHE_ENABLED=true
      depends_on:
        postgres:
          condition: service_healthy
        redis:
          condition: service_started
      networks:
        - e2e-test-network
      healthcheck:
        test: ["CMD", "curl", "-f", "http://localhost:4444/health"]
        interval: 10s
        timeout: 5s
        retries: 5
    
    volumes:
      redis-data:
      postgres-data:
    
    networks:
      e2e-test-network:
        driver: bridge
  • 4.2 Full stack integration tests tests/e2e/test_full_stack.py

    # Complete end-to-end stack testing
    import pytest
    import asyncio
    import json
    import httpx
    import psycopg2
    import redis
    from tests.e2e.fixtures import E2ETestHarness
    
    class TestFullStackIntegration:
        """Test complete stack integration"""
    
        @pytest.fixture
        def test_harness(self):
            return E2ETestHarness()
    
        @pytest.mark.asyncio
        async def test_complete_e2e_flow(self, test_harness):
            """Test complete end-to-end flow with all components"""
            # 1. Setup - register time server with gateway
            token = await test_harness.generate_test_token()
            
            # Register fast-time-server (SSE mode) with gateway
            async with httpx.AsyncClient() as client:
                gateway_response = await client.post(
                    "http://localhost:4444/gateways",
                    headers={"Authorization": f"Bearer {token}"},
                    json={
                        "name": "fast-time-sse",
                        "url": "http://fast-time-sse:8001/sse"
                    }
                )
                assert gateway_response.status_code == 201
                gateway_id = gateway_response.json()["id"]
    
                # Create virtual server
                server_response = await client.post(
                    "http://localhost:4444/servers",
                    headers={"Authorization": f"Bearer {token}"},
                    json={
                        "name": "e2e-test-server",
                        "description": "E2E test server",
                        "associatedTools": ["1"]  # Assuming tool ID 1
                    }
                )
                assert server_response.status_code == 201
                server_id = server_response.json()["id"]
    
            # 2. Test via wrapper
            wrapper_process = await test_harness.start_wrapper_with_token(token)
            
            try:
                # Initialize
                init_response = await test_harness.send_stdio_message(wrapper_process, {
                    "jsonrpc": "2.0", "id": 1, "method": "initialize",
                    "params": {"protocolVersion": "2025-03-26", "capabilities": {}}
                })
                assert "result" in init_response
    
                # List tools
                tools_response = await test_harness.send_stdio_message(wrapper_process, {
                    "jsonrpc": "2.0", "id": 2, "method": "tools/list"
                })
                assert "result" in tools_response
                tools = tools_response["result"]["tools"]
                assert len(tools) > 0
                assert any(tool["name"] == "get_current_time" for tool in tools)
    
                # Call tool
                call_response = await test_harness.send_stdio_message(wrapper_process, {
                    "jsonrpc": "2.0", "id": 3, "method": "tools/call",
                    "params": {"name": "get_current_time", "arguments": {"timezone": "UTC"}}
                })
                assert "result" in call_response
                assert "content" in call_response["result"]
    
            finally:
                await test_harness.stop_wrapper(wrapper_process)
    
            # 3. Verify data persistence
            await test_harness.verify_postgres_state(server_id, gateway_id)
            await test_harness.verify_redis_cache()
    
        @pytest.mark.asyncio
        async def test_cache_consistency(self, test_harness):
            """Test L1/L2 cache consistency"""
            # Test cache behavior across multiple requests
            pass
    
        @pytest.mark.asyncio
        async def test_federation_integration(self, test_harness):
            """Test federation with multiple gateway instances"""
            # Test federation discovery and tool aggregation
            pass

Phase 5: GitHub Actions CI Integration

  • 5.1 E2E testing workflow .github/workflows/e2e-tests.yml
    name: End-to-End Stack Testing
    
    on:
      push:
        branches: [main, develop]
      pull_request:
        branches: [main]
      schedule:
        - cron: '0 6 * * *'  # Daily at 6 AM UTC
    
    jobs:
      e2e-matrix:
        runs-on: ubuntu-latest
        strategy:
          matrix:
            test-suite: [minimal, basic, standard, enterprise]
            include:
              - test-suite: minimal
                time-server-modes: "stdio"
                data-stores: "sqlite"
                duration: "5m"
              - test-suite: basic
                time-server-modes: "stdio,sse"
                data-stores: "redis,sqlite"
                duration: "10m"
              - test-suite: standard
                time-server-modes: "stdio,sse,http"
                data-stores: "redis,postgresql"
                duration: "20m"
              - test-suite: enterprise
                time-server-modes: "stdio,sse,http"
                data-stores: "redis,postgresql"
                duration: "45m"
                federation: true
          fail-fast: false
    
        steps:
          - uses: actions/checkout@v4
    
          - name: Set up Docker Buildx
            uses: docker/setup-buildx-action@v3
    
          - name: Generate test JWT token
            run: |
              export E2E_TEST_TOKEN=$(python3 -c "
              import jwt
              import time
              token = jwt.encode({
                  'username': 'e2e-test',
                  'exp': int(time.time()) + 3600
              }, 'e2e-test-secret-key', algorithm='HS256')
              print(token)
              ")
              echo "E2E_TEST_TOKEN=$E2E_TEST_TOKEN" >> $GITHUB_ENV
    
          - name: Start E2E test stack
            run: |
              # Configure stack based on test suite
              export TEST_SUITE=${{ matrix.test-suite }}
              export TIME_SERVER_MODES="${{ matrix.time-server-modes }}"
              export DATA_STORES="${{ matrix.data-stores }}"
              
              # Start stack
              docker-compose -f docker-compose.e2e.yml up -d
              
              # Wait for health checks
              timeout 300s bash -c 'until docker-compose -f docker-compose.e2e.yml ps | grep -q "healthy"; do sleep 10; done'
    
          - name: Run E2E test suite
            timeout-minutes: ${{ fromJSON('{"minimal": 10, "basic": 15, "standard": 30, "enterprise": 60}')[matrix.test-suite] }}
            run: |
              # Run tests based on suite
              case "${{ matrix.test-suite }}" in
                minimal)
                  pytest tests/e2e/test_basic_integration.py -v --tb=short
                  ;;
                basic)
                  pytest tests/e2e/test_protocol_matrix.py tests/e2e/test_auth_flow.py -v --tb=short
                  ;;
                standard)
                  pytest tests/e2e/ -v --tb=short -k "not enterprise"
                  ;;
                enterprise)
                  pytest tests/e2e/ -v --tb=short
                  ;;
              esac
    
          - name: Validate data persistence
            run: |
              # Check database state
              docker exec postgres psql -U test -d mcpgateway_e2e -c "SELECT COUNT(*) FROM servers;"
              docker exec postgres psql -U test -d mcpgateway_e2e -c "SELECT COUNT(*) FROM tools;"
              
              # Check Redis cache
              docker exec redis redis-cli DBSIZE
    
          - name: Collect logs and artifacts
            if: always()
            run: |
              mkdir -p artifacts/${{ matrix.test-suite }}
              
              # Collect container logs
              docker-compose -f docker-compose.e2e.yml logs > artifacts/${{ matrix.test-suite }}/docker-logs.txt
              
              # Collect test reports
              cp -r test-reports/ artifacts/${{ matrix.test-suite }}/ || true
              
              # Export database state
              docker exec postgres pg_dump -U test mcpgateway_e2e > artifacts/${{ matrix.test-suite }}/database-dump.sql || true
    
          - name: Upload artifacts
            uses: actions/upload-artifact@v4
            if: always()
            with:
              name: e2e-test-${{ matrix.test-suite }}-${{ github.sha }}
              retention-days: 14
              path: artifacts/
    
          - name: Cleanup
            if: always()
            run: |
              docker-compose -f docker-compose.e2e.yml down -v
              docker system prune -f

Phase 6: Test Utilities and Fixtures

  • 6.1 E2E test harness tests/e2e/fixtures.py
    # E2E test utilities and fixtures
    import asyncio
    import json
    import subprocess
    import time
    from typing import Any, Dict, Optional
    import httpx
    import jwt
    import psycopg2
    import redis
    
    class E2ETestHarness:
        """Comprehensive E2E testing utilities"""
    
        def __init__(self):
            self.jwt_secret = "e2e-test-secret-key"
            self.gateway_url = "http://localhost:4444"
            self.redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
            self.postgres_conn = None
    
        async def generate_test_token(self, username: str = "e2e-test", exp: int = 3600) -> str:
            """Generate JWT token for testing"""
            payload = {
                "username": username,
                "exp": int(time.time()) + exp
            }
            return jwt.encode(payload, self.jwt_secret, algorithm="HS256")
    
        async def start_wrapper_with_token(self, token: str) -> subprocess.Popen:
            """Start wrapper process with authentication token"""
            env = {
                "MCP_AUTH_TOKEN": token,
                "MCP_SERVER_CATALOG_URLS": f"{self.gateway_url}/servers/1",
                "LOG_LEVEL": "debug"
            }
            
            process = subprocess.Popen(
                ["python", "-m", "mcpgateway.wrapper"],
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                env=env
            )
            
            # Wait for wrapper to initialize
            await asyncio.sleep(2)
            return process
    
        async def send_stdio_message(self, process: subprocess.Popen, message: Dict[str, Any]) -> Dict[str, Any]:
            """Send JSON-RPC message via stdio and get response"""
            message_str = json.dumps(message) + "\n"
            process.stdin.write(message_str)
            process.stdin.flush()
            
            # Read response
            response_line = process.stdout.readline()
            return json.loads(response_line.strip())
    
        async def stop_wrapper(self, process: subprocess.Popen):
            """Stop wrapper process gracefully"""
            process.terminate()
            try:
                process.wait(timeout=5)
            except subprocess.TimeoutExpired:
                process.kill()
    
        async def verify_postgres_state(self, server_id: int, gateway_id: int):
            """Verify database state after tests"""
            if not self.postgres_conn:
                self.postgres_conn = psycopg2.connect(
                    host="localhost",
                    database="mcpgateway_e2e",
                    user="test",
                    password="test"
                )
            
            cursor = self.postgres_conn.cursor()
            
            # Check server exists
            cursor.execute("SELECT COUNT(*) FROM servers WHERE id = %s", (server_id,))
            assert cursor.fetchone()[0] == 1
            
            # Check gateway exists
            cursor.execute("SELECT COUNT(*) FROM gateways WHERE id = %s", (gateway_id,))
            assert cursor.fetchone()[0] == 1
    
        async def verify_redis_cache(self):
            """Verify Redis cache state"""
            # Check cache keys exist
            cache_keys = self.redis_client.keys("*")
            assert len(cache_keys) > 0
            
            # Check specific cached data
            tools_cache = self.redis_client.get("tools:list")
            assert tools_cache is not None
    
        async def test_protocol_translation(self, source: str, target: str) -> 'TranslationResult':
            """Test protocol translation between source and target"""
            # Implementation for testing protocol translations
            start_time = time.time()
            
            # Perform translation test based on protocols
            success = True  # Placeholder
            response_time = time.time() - start_time
            data_integrity = True  # Placeholder
            
            return TranslationResult(
                success=success,
                response_time=response_time,
                data_integrity_check_passed=data_integrity
            )
    
    class TranslationResult:
        def __init__(self, success: bool, response_time: float, data_integrity_check_passed: bool):
            self.success = success
            self.response_time = response_time
            self.data_integrity_check_passed = data_integrity_check_passed

Phase 7: Enhanced Makefile Targets

  • 7.1 E2E testing make targets
    # E2E testing targets
    e2e-test-minimal:
    	@echo "πŸ§ͺ Running minimal E2E test suite..."
    	@docker-compose -f docker-compose.e2e.yml up -d redis postgres
    	@$(MAKE) _wait_for_services
    	@pytest tests/e2e/test_basic_integration.py -v
    	@docker-compose -f docker-compose.e2e.yml down -v
    
    e2e-test-basic:
    	@echo "πŸ§ͺ Running basic E2E test suite..."
    	@docker-compose -f docker-compose.e2e.yml up -d
    	@$(MAKE) _wait_for_services
    	@pytest tests/e2e/test_protocol_matrix.py tests/e2e/test_auth_flow.py -v
    	@docker-compose -f docker-compose.e2e.yml down -v
    
    e2e-test-standard:
    	@echo "πŸ§ͺ Running standard E2E test suite..."
    	@docker-compose -f docker-compose.e2e.yml up -d
    	@$(MAKE) _wait_for_services
    	@pytest tests/e2e/ -v -k "not enterprise"
    	@docker-compose -f docker-compose.e2e.yml down -v
    
    e2e-test-enterprise:
    	@echo "πŸ§ͺ Running enterprise E2E test suite..."
    	@docker-compose -f docker-compose.e2e.yml up -d
    	@$(MAKE) _wait_for_services
    	@pytest tests/e2e/ -v
    	@docker-compose -f docker-compose.e2e.yml down -v
    
    e2e-test-all:
    	@echo "πŸ§ͺ Running ALL E2E test suites..."
    	@$(MAKE) e2e-test-minimal
    	@$(MAKE) e2e-test-basic
    	@$(MAKE) e2e-test-standard
    	@$(MAKE) e2e-test-enterprise
    	@echo "βœ… All E2E tests completed"
    
    test-protocol-matrix:
    	@echo "πŸ”„ Testing protocol translation matrix..."
    	@docker-compose -f docker-compose.e2e.yml up -d fast-time-stdio fast-time-sse fast-time-http translate-stdio-to-sse
    	@pytest tests/e2e/test_protocol_matrix.py -v
    	@docker-compose -f docker-compose.e2e.yml down -v
    
    test-full-stack:
    	@echo "πŸ—οΈ Testing complete stack integration..."
    	@docker-compose -f docker-compose.e2e.yml up -d
    	@$(MAKE) _wait_for_services
    	@pytest tests/e2e/test_full_stack.py -v
    	@docker-compose -f docker-compose.e2e.yml down -v
    
    _wait_for_services:
    	@echo "⏳ Waiting for services to be healthy..."
    	@timeout 300s bash -c 'until docker-compose -f docker-compose.e2e.yml ps | grep -q "healthy"; do sleep 5; echo "Waiting..."; done'
    	@echo "βœ… All services are healthy"

πŸ“¦ Updated Deliverables

  1. Multi-protocol fast-time-server: Complete implementation supporting stdio, SSE, and streamable HTTP
  2. Protocol translation matrix: Comprehensive testing of all translation combinations
  3. Authentication flow validation: OAuth2 Bearer token security testing through wrapper
  4. Full stack Docker composition: Complete E2E environment with all components
  5. Comprehensive test suites: 4-tier testing from minimal to enterprise scale
  6. GitHub Actions integration: Matrix CI testing across all component combinations
  7. Test utilities and fixtures: Reusable E2E testing infrastructure
  8. Enhanced documentation: Complete E2E testing guide with troubleshooting

🎯 Expected E2E Outcomes

Integration Validation:

  • Complete flow coverage: All client β†’ wrapper β†’ gateway β†’ backend β†’ storage paths tested
  • Protocol translation accuracy: 100% success rate across all 9 protocol combinations
  • Authentication security: OAuth2 Bearer token validation with proper error handling
  • Data consistency: L1/L2 cache and database persistence verified across all flows

Performance Baselines:

  • E2E latency: <500ms for complete client-to-backend-to-response cycle
  • Protocol translation overhead: <50ms additional latency per translation step
  • Authentication overhead: <100ms for token validation and session establishment
  • Cache efficiency: >95% hit rate for repeated tool calls within session

Production Readiness Validation:

  • Component health monitoring: All services report healthy status
  • Error handling: Graceful degradation when components fail
  • Federation functionality: Multi-gateway federation works reliably
  • Security compliance: Authentication and authorization properly enforced

🧩 Additional Notes

  • Real-world usage patterns: Tests simulate actual MCP client behavior (Claude Desktop, Inspector)
  • Protocol fidelity: Translation preserves all MCP protocol semantics and error conditions
  • Security-first approach: Authentication tested at every layer with proper token validation
  • Production scenarios: Tests include component failures, network issues, and high load
  • CI scalability: Matrix testing enables rapid validation of new features across all combinations
  • Debugging support: Comprehensive logging and artifact collection for troubleshooting
  • Documentation completeness: Step-by-step guides for running, debugging, and extending E2E tests

Metadata

Metadata

Assignees

Labels

choreLinting, formatting, dependency hygiene, or project maintenance chorescicdIssue with CI/CD process (GitHub Actions, scaffolding)devopsDevOps activities (containers, automation, deployment, makefiles, etc)help wantedExtra attention is neededtriageIssues / Features awaiting triage

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions