Files

21 KiB

Quick Start Guide: Mock GDS MCP Server

Branch: 001-mock-gds-server | Date: 2026-04-07

Overview

This guide demonstrates how to use the Mock GDS MCP Server for testing travel applications. All examples use mock data with the TEST- prefix for safety.


Installation & Setup

Prerequisites

  • Node.js 20 LTS or later
  • Docker & Docker Compose (for Valkey)
  • MCP-compatible client

Running with Docker Compose

# Start the mock GDS server and Valkey
docker-compose up -d

# View logs
docker-compose logs -f gds-mock-mcp

# Stop services
docker-compose down

Configuration

Create a .env file (optional, defaults shown):

# MCP Server
MCP_TRANSPORT=stdio
MCP_SESSION_TIMEOUT=3600

# Valkey
VALKEY_HOST=valkey
VALKEY_PORT=6379
VALKEY_PASSWORD=
VALKEY_DB=0

# Logging
LOG_LEVEL=info
LOG_PRETTY=false

# Mock Data
MOCK_DATA_SEED=fixed
MOCK_RESPONSE_DELAY=0

Basic Workflows

1. Search and Book a Flight

Step 1: Search for flights

// MCP tool call
{
  "tool": "searchFlights",
  "arguments": {
    "origin": "JFK",
    "destination": "LAX",
    "departureDate": "2026-06-15",
    "passengers": { "adults": 1 },
    "cabin": "economy"
  }
}

// Response
{
  "searchId": "search_abc123",
  "flights": [
    {
      "id": "flight_1",
      "flightNumber": "AA123",
      "airline": { "code": "AA", "name": "American Airlines" },
      "departure": { "time": "2026-06-15T08:00:00Z", "airport": "JFK" },
      "arrival": { "time": "2026-06-15T11:30:00Z", "airport": "LAX" },
      "price": 29900,  // $299.00
      "seatsAvailable": 45,
      "status": "available"
    },
    // ... more flights
  ],
  "resultCount": 5
}

Step 2: Book the selected flight

// MCP tool call
{
  "tool": "bookFlight",
  "arguments": {
    "flightIds": ["flight_1"],
    "passengers": [
      {
        "type": "adult",
        "firstName": "John",
        "lastName": "Doe",
        "email": "john.doe@example.com"
      }
    ],
    "contactEmail": "john.doe@example.com"
  }
}

// Response
{
  "pnr": "TEST-A1B2C3",  // Note: TEST- prefix for safety
  "status": "confirmed",
  "bookingDate": "2026-04-07T16:30:00Z",
  "passengers": [{ "firstName": "John", "lastName": "Doe" }],
  "flights": [{ /* flight details */ }],
  "totalPrice": 29900,
  "currency": "USD"
}

2. Multi-Service Booking (Flight + Hotel)

Step 1: Book a flight (as above)

Result: pnr = "TEST-A1B2C3"

Step 2: Search for hotels

{
  "tool": "searchHotels",
  "arguments": {
    "cityCode": "LAX",
    "checkInDate": "2026-06-15",
    "checkOutDate": "2026-06-17",
    "guests": 1
  }
}

// Response
{
  "hotels": [
    {
      "id": "hotel_1",
      "name": "Marriott Los Angeles Downtown",
      "starRating": 4,
      "pricePerNight": 18000,  // $180.00
      "totalPrice": 36000,     // 2 nights
      "amenities": ["WiFi", "Pool", "Gym", "Parking"]
    }
  ]
}

Step 3: Add hotel to existing booking

{
  "tool": "bookHotel",
  "arguments": {
    "hotelId": "hotel_1",
    "existingPnr": "TEST-A1B2C3",  // Add to flight booking
    "guests": [
      {
        "firstName": "John",
        "lastName": "Doe",
        "email": "john.doe@example.com"
      }
    ]
  }
}

// Response
{
  "pnr": "TEST-A1B2C3",  // Same PNR, now includes hotel
  "flights": [{ /* flight */ }],
  "hotels": [{ /* hotel */ }],
  "totalPrice": 65900  // $299 flight + $360 hotel
}

3. Complete Travel Package (Flight + Hotel + Car)

Step 1-2: Book flight and hotel (as above)

Result: pnr = "TEST-A1B2C3"

Step 3: Search for rental cars

{
  "tool": "searchCars",
  "arguments": {
    "pickupLocationCode": "LAX",
    "pickupDate": "2026-06-15T12:00:00Z",
    "dropoffDate": "2026-06-17T10:00:00Z"
  }
}

// Response
{
  "cars": [
    {
      "id": "car_1",
      "companyName": "Hertz",
      "vehicleClass": "economy",
      "vehicleModel": "Toyota Corolla or similar",
      "dailyRate": 4500,   // $45/day
      "totalPrice": 9000,  // 2 days
      "mileagePolicy": "unlimited"
    }
  ]
}

Step 4: Add car to booking

{
  "tool": "bookCar",
  "arguments": {
    "carId": "car_1",
    "existingPnr": "TEST-A1B2C3",
    "driver": {
      "firstName": "John",
      "lastName": "Doe",
      "age": 35
    }
  }
}

// Response
{
  "pnr": "TEST-A1B2C3",
  "flights": [{ /* flight */ }],
  "hotels": [{ /* hotel */ }],
  "cars": [{ /* car */ }],
  "totalPrice": 74900  // $299 + $360 + $90
}

4. Retrieve and Manage Bookings

Retrieve a booking by PNR

{
  "tool": "retrieveBooking",
  "arguments": {
    "pnr": "TEST-A1B2C3"
  }
}

// Response: Full booking details

List all bookings in session

{
  "tool": "listBookings",
  "arguments": {
    "status": "all"
  }
}

// Response
{
  "bookings": [
    {
      "pnr": "TEST-A1B2C3",
      "status": "confirmed",
      "createdAt": "2026-04-07T16:30:00Z",
      "passengerCount": 1,
      "segmentCounts": { "flights": 1, "hotels": 1, "cars": 1 },
      "totalPrice": 74900
    }
  ],
  "totalCount": 1
}

Cancel a booking

{
  "tool": "cancelBooking",
  "arguments": {
    "pnr": "TEST-A1B2C3",
    "reason": "Travel plans changed"
  }
}

// Response
{
  "pnr": "TEST-A1B2C3",
  "status": "cancelled",
  "cancelledAt": "2026-04-07T17:00:00Z"
}

Testing Scenarios

Concurrent Sessions

// Session 1
const session1 = createMCPConnection();
const booking1 = await session1.call("bookFlight", { /* ... */ });
// booking1.pnr = "TEST-A1B2C3"

// Session 2 (different MCP connection)
const session2 = createMCPConnection();
const booking2 = await session2.call("bookFlight", { /* ... */ });
// booking2.pnr = "TEST-X7Y8Z9"  // Different PNR

// Sessions are isolated - cannot retrieve booking1 from session2
await session2.call("retrieveBooking", { pnr: "TEST-A1B2C3" });
// Error: PNR not found in current session

Error Handling

Invalid airport code

{
  "tool": "searchFlights",
  "arguments": {
    "origin": "ZZZ",  // Invalid code
    "destination": "LAX",
    "departureDate": "2026-06-15"
  }
}

// Error response
{
  "code": -32602,
  "message": "Invalid airport code 'ZZZ': must be a valid 3-letter IATA code",
  "data": { "field": "origin", "value": "ZZZ" }
}

Date in the past

{
  "tool": "searchFlights",
  "arguments": {
    "origin": "JFK",
    "destination": "LAX",
    "departureDate": "2020-01-01"  // Past date
  }
}

// Error response
{
  "code": -32002,
  "message": "Departure date cannot be in the past",
  "data": { "field": "departureDate", "value": "2020-01-01" }
}

Cancelled booking modification

// First cancel
await call("cancelBooking", { pnr: "TEST-A1B2C3" });

// Try to add hotel to cancelled booking
await call("bookHotel", {
  hotelId: "hotel_1",
  existingPnr: "TEST-A1B2C3"
});

// Error response
{
  "code": -32002,
  "message": "Cannot modify cancelled booking TEST-A1B2C3",
  "data": { "pnr": "TEST-A1B2C3", "status": "cancelled" }
}

Integration Testing Example

// Example integration test (using Node.js test runner)
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { MCPClient } from './mcp-client.js';

test('complete booking workflow', async (t) => {
  const client = new MCPClient();
  
  // Search flights
  const searchResults = await client.call('searchFlights', {
    origin: 'JFK',
    destination: 'LAX',
    departureDate: '2026-06-15',
    passengers: { adults: 1 }
  });
  
  assert.ok(searchResults.flights.length > 0);
  const flight = searchResults.flights[0];
  
  // Book flight
  const booking = await client.call('bookFlight', {
    flightIds: [flight.id],
    passengers: [{
      type: 'adult',
      firstName: 'Test',
      lastName: 'User',
      email: 'test@example.com'
    }],
    contactEmail: 'test@example.com'
  });
  
  assert.match(booking.pnr, /^TEST-[A-Z0-9]{6}$/);
  assert.equal(booking.status, 'confirmed');
  
  // Retrieve booking
  const retrieved = await client.call('retrieveBooking', {
    pnr: booking.pnr
  });
  
  assert.equal(retrieved.pnr, booking.pnr);
  assert.equal(retrieved.flights.length, 1);
  
  // Cancel booking
  const cancelled = await client.call('cancelBooking', {
    pnr: booking.pnr
  });
  
  assert.equal(cancelled.status, 'cancelled');
});

Mock Data Reference

Supported Airports

Major US: JFK, LAX, ORD, DFW, ATL, MIA, SFO, SEA, BOS, LAS
International: LHR, CDG, FRA, NRT, HKG, SYD, DXB

Supported Airlines

  • American Airlines (AA)
  • Delta Air Lines (DL)
  • United Airlines (UA)
  • Southwest Airlines (WN)
  • British Airways (BA)
  • Virgin Atlantic (VS)
  • Lufthansa (LH)
  • Air France (AF)

Price Ranges

  • Flights: $200-$800 (economy domestic), $800-$2000 (business), $2500+ (first)
  • Hotels: $80-$150 (budget), $150-$300 (mid-range), $300-$800 (luxury)
  • Cars: $35-$50 (economy), $50-$80 (midsize), $100-$150 (luxury)

Troubleshooting

Server won't start

Check Valkey connection:

docker-compose logs valkey

Session expires

Default timeout: 1 hour. Adjust with MCP_SESSION_TIMEOUT env var.

PNR not found

Verify you're retrieving from the same session that created the booking.


Next Steps

  • Explore all available tools in /contracts/mcp-tools.md
  • Review data model in /data-model.md
  • Check technical research in /research.md
  • See implementation tasks in /tasks.md (after running /speckit.tasks)

Remote Access Setup (HTTP/2 Mode)

The mock GDS server can be accessed remotely over HTTP/2, enabling distributed teams and web-based MCP clients.

Remote Mode Configuration

Update .env for remote access:

# Transport Mode
TRANSPORT_MODE=http              # Enable HTTP transport (or 'both' for stdio + http)

# HTTP Server
HTTP_PORT=3000                   # Internal HTTP port (proxied by nginx)
HTTP_HOST=127.0.0.1             # Bind to localhost (nginx only)

# Rate Limiting
RATE_LIMIT_ENABLED=true
RATE_LIMIT_PER_MINUTE=100       # 100 requests per minute per IP
RATE_LIMIT_WINDOW_SECONDS=60

# PNR/Session Timeouts
PNR_TTL_HOURS=1                 # Global PNR retrieval window
SESSION_TTL_HOURS=24            # Session expiration

# CORS
CORS_ENABLED=true
CORS_ORIGINS=*                  # Wildcard for development
CORS_MAX_AGE=86400

Docker Compose Setup (Remote Mode)

docker-compose.yml (with nginx reverse proxy):

version: '3.8'

services:
  valkey:
    image: valkey/valkey:8.0-alpine
    ports:
      - "6379:6379"
    volumes:
      - valkey-data:/data
    command: valkey-server --appendonly yes
  
  mcp-server:
    build: .
    environment:
      TRANSPORT_MODE: http
      HTTP_PORT: 3000
      HTTP_HOST: 0.0.0.0
      VALKEY_HOST: valkey
      VALKEY_PORT: 6379
      RATE_LIMIT_ENABLED: "true"
      RATE_LIMIT_PER_MINUTE: 100
      PNR_TTL_HOURS: 1
      LOG_LEVEL: info
    depends_on:
      - valkey
    ports:
      - "3000:3000"  # For development without nginx
  
  nginx:
    image: nginx:alpine
    ports:
      - "8080:8080"  # External HTTP/2 port
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    depends_on:
      - mcp-server

volumes:
  valkey-data:

Nginx Configuration

nginx/nginx.conf:

events {
    worker_connections 1024;
}

http {
    upstream mcp_backend {
        server mcp-server:3000;
    }
    
    server {
        listen 8080 ssl http2;
        server_name localhost;
        
        # TLS Configuration
        ssl_certificate /etc/nginx/certs/cert.pem;
        ssl_certificate_key /etc/nginx/certs/key.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        
        # MCP Endpoints
        location /mcp {
            proxy_pass http://mcp_backend;
            proxy_http_version 1.1;
            
            # Headers for rate limiting and logging
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $host;
            
            # SSE Support (for MCP streaming)
            proxy_buffering off;
            proxy_cache off;
            chunked_transfer_encoding on;
            proxy_read_timeout 3600s;
            
            # CORS (optional - can be handled by app)
            add_header Access-Control-Allow-Origin * always;
            add_header Access-Control-Expose-Headers 'X-RateLimit-Limit, X-RateLimit-Remaining' always;
        }
        
        # Health Check
        location /health {
            proxy_pass http://mcp_backend/health;
            proxy_http_version 1.1;
            access_log off;  # Don't log health checks
        }
    }
}

Generate Self-Signed Certificate (Development)

# Create certs directory
mkdir -p nginx/certs

# Generate self-signed certificate (valid 365 days)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout nginx/certs/key.pem \
  -out nginx/certs/cert.pem \
  -subj "/C=US/ST=Dev/L=Local/O=MockGDS/CN=localhost"

# Set permissions
chmod 644 nginx/certs/cert.pem
chmod 600 nginx/certs/key.pem

Start Remote Server

# Start all services
docker-compose up -d

# Check status
docker-compose ps

# View logs
docker-compose logs -f mcp-server nginx

# Test health endpoint
curl -k https://localhost:8080/health

# Test CORS
curl -i -H "Origin: https://example.com" -X OPTIONS https://localhost:8080/mcp

Connecting Remote MCP Clients

Web-Based Client (Browser)

// Example: Connect from browser-based MCP client
const mcpClient = new MCPClient({
  url: 'https://localhost:8080/mcp',
  transport: 'sse',  // Server-sent events
  sessionId: generateSessionId()  // Or let server generate
});

await mcpClient.connect();

// Call tools
const flights = await mcpClient.callTool('searchFlights', {
  origin: 'JFK',
  destination: 'LAX',
  departureDate: '2026-06-15',
  passengers: { adults: 1 },
  cabin: 'economy'
});

console.log(`Found ${flights.resultCount} flights`);

cURL Examples

Search flights:

curl -k -X POST https://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Session-ID: $(uuidgen)" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "searchFlights",
      "arguments": {
        "origin": "JFK",
        "destination": "LAX",
        "departureDate": "2026-06-15",
        "passengers": { "adults": 1 },
        "cabin": "economy"
      }
    },
    "id": 1
  }'

Check health:

curl -k https://localhost:8080/health | jq

Check rate limits:

# Make multiple requests and check headers
for i in {1..5}; do
  curl -k -i https://localhost:8080/health 2>&1 | grep -i ratelimit
done

Rate Limiting Examples

Normal Usage

# First request
curl -i https://localhost:8080/health
# Returns:
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 99
# X-RateLimit-Reset: 1712486520

Rate Limit Exceeded

# Make 101 requests in under 60 seconds
for i in {1..101}; do
  curl -s https://localhost:8080/health > /dev/null
done

# 101st request returns:
# HTTP/1.1 429 Too Many Requests
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 0
# X-RateLimit-Reset: 1712486520
# Retry-After: 15
# 
# {
#   "error": "Rate limit exceeded",
#   "code": "RATE_LIMIT_EXCEEDED",
#   "limit": 100,
#   "current": 101,
#   "resetAt": "2026-04-07T10:02:00.000Z",
#   "retryAfter": 15
# }

Waiting for Reset

# Wait until reset time
sleep 15

# Try again (should work)
curl https://localhost:8080/health
# Returns 200 OK with fresh rate limit

Global PNR Retrieval Demo

Remote mode enables cross-session PNR retrieval for testing flexibility.

Session 1: Create Booking

SESSION1=$(uuidgen)

curl -k -X POST https://localhost:8080/mcp \
  -H "MCP-Session-ID: $SESSION1" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "bookFlight",
      "arguments": {
        "flightId": "flight_1",
        "passengers": [{
          "type": "adult",
          "firstName": "John",
          "lastName": "Doe",
          "email": "john@example.com"
        }]
      }
    },
    "id": 1
  }'

# Returns: { "pnr": "TEST-ABC123", ... }

Session 2: Retrieve Same PNR

SESSION2=$(uuidgen)

curl -k -X POST https://localhost:8080/mcp \
  -H "MCP-Session-ID: $SESSION2" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "retrieveBooking",
      "arguments": {
        "pnr": "TEST-ABC123"
      }
    },
    "id": 1
  }'

# Returns: Full PNR details (cross-session retrieval works!)

After 1 Hour: PNR Expired

# Wait 1 hour (or set PNR_TTL_HOURS=0.01 for 36 seconds)
sleep 3660

curl -k -X POST https://localhost:8080/mcp \
  -H "MCP-Session-ID: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "retrieveBooking",
      "arguments": { "pnr": "TEST-ABC123" }
    },
    "id": 1
  }'

# Returns:
# {
#   "error": {
#     "code": -32001,
#     "message": "PNR TEST-ABC123 not found or expired"
#   }
# }

Security Recommendations

Development Environment

Safe for local development:

  • Use on localhost or private networks
  • Self-signed certificates acceptable
  • Wildcard CORS appropriate for testing

Shared Development Server

⚠️ Use network-level security:

# Option 1: VPN Access
# Deploy server within VPN, require VPN connection

# Option 2: Firewall Rules
# Allow only specific IP ranges
iptables -A INPUT -p tcp --dport 8080 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j DROP

# Option 3: Private Network (AWS/GCP)
# Deploy in private subnet, access via bastion host or VPN

Production-Like Environment

DO NOT deploy to public internet without authentication:

  • Mock server has no authentication (by design for v1)
  • Wildcard CORS allows any origin
  • Contains test data but still inappropriate for public exposure

Future Enhancement: Authentication layer for production deployments


Monitoring & Observability

Health Check Integration

Kubernetes Liveness Probe:

livenessProbe:
  httpGet:
    path: /health
    port: 8080
    scheme: HTTPS
  initialDelaySeconds: 30
  periodSeconds: 10

Prometheus Scraping:

scrape_configs:
  - job_name: 'mcp-server'
    scheme: https
    tls_config:
      insecure_skip_verify: true
    static_configs:
      - targets: ['localhost:8080']
    metrics_path: '/health'

Log Aggregation

View structured logs:

docker-compose logs -f mcp-server | jq

Filter by session:

docker-compose logs mcp-server | grep "sessionId:abc-123"

Monitor rate limits:

docker-compose logs mcp-server | grep "RATE_LIMIT_EXCEEDED"

Troubleshooting (Remote Mode)

Connection Refused

# Check if server is running
docker-compose ps

# Check if port is exposed
netstat -an | grep 8080

# Check nginx logs
docker-compose logs nginx

CORS Errors in Browser

// Console error: "Access-Control-Allow-Origin"
// Check CORS headers:
curl -i -H "Origin: https://example.com" https://localhost:8080/mcp
# Should include: Access-Control-Allow-Origin: *

Rate Limit Too Restrictive

# Increase limit in .env
RATE_LIMIT_PER_MINUTE=1000

# Restart server
docker-compose restart mcp-server

SSL Certificate Warnings

# Browser: "Your connection is not private"
# Expected with self-signed cert

# Production: Use Let's Encrypt
certbot certonly --standalone -d your-domain.com
# Update nginx.conf with real certificate paths

PNR Not Found (Cross-Session)

# Verify PNR is within TTL window
curl https://localhost:8080/health | jq .uptime
# If uptime > 3600 and PNR_TTL_HOURS=1, PNR may have expired

# Check Valkey directly
docker-compose exec valkey valkey-cli
> KEYS pnr:*
> TTL pnr:TEST-ABC123

Performance Testing (Remote Mode)

Load Testing with ab (Apache Bench)

# Test health endpoint (10000 requests, 100 concurrent)
ab -n 10000 -c 100 -k https://localhost:8080/health

# Expected: <2s for 10k requests

Rate Limit Testing

# Verify rate limiting kicks in
ab -n 200 -c 1 https://localhost:8080/health

# Should see ~100 successful, ~100 rate limited (429)

Concurrent Sessions

# Simulate 50 concurrent sessions
for i in {1..50}; do
  (
    SESSION=$(uuidgen)
    curl -k -X POST https://localhost:8080/mcp \
      -H "MCP-Session-ID: $SESSION" \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' &
  )
done
wait

# Check health to see connection count
curl -k https://localhost:8080/health | jq .connections

Next Steps (Remote Mode)

  1. Set up Docker Compose with nginx reverse proxy
  2. Generate TLS certificates for development
  3. Configure environment variables for remote access
  4. Test health endpoint and CORS
  5. Verify rate limiting behavior
  6. Test cross-session PNR retrieval
  7. Monitor logs and health metrics
  8. 🔄 Integrate with your web-based MCP client
  9. 🔄 Deploy to shared development environment (with VPN/firewall)
  10. 📋 Plan authentication layer for future production use