-
Notifications
You must be signed in to change notification settings - Fork 215
Description
π§ 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:
- Multi-protocol fast-time-server deployment (stdio, SSE, streamable HTTP)
- Protocol translation matrix testing with mcpgateway.translate (any-to-any conversion)
- Wrapper authentication flow validation (stdio β SSE with OAuth2 Bearer)
- Full gateway stack with Redis cache and PostgreSQL persistence
- GitHub Actions integration with matrix testing across all component combinations
- 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
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
π 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
π 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
- Multi-protocol fast-time-server: Complete implementation supporting stdio, SSE, and streamable HTTP
- Protocol translation matrix: Comprehensive testing of all translation combinations
- Authentication flow validation: OAuth2 Bearer token security testing through wrapper
- Full stack Docker composition: Complete E2E environment with all components
- Comprehensive test suites: 4-tier testing from minimal to enterprise scale
- GitHub Actions integration: Matrix CI testing across all component combinations
- Test utilities and fixtures: Reusable E2E testing infrastructure
- 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