1077 lines
21 KiB
Markdown
1077 lines
21 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# 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):
|
|
|
|
```bash
|
|
# 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**
|
|
|
|
```javascript
|
|
// 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**
|
|
|
|
```javascript
|
|
// 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**
|
|
|
|
```javascript
|
|
{
|
|
"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**
|
|
|
|
```javascript
|
|
{
|
|
"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**
|
|
|
|
```javascript
|
|
{
|
|
"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**
|
|
|
|
```javascript
|
|
{
|
|
"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**
|
|
|
|
```javascript
|
|
{
|
|
"tool": "retrieveBooking",
|
|
"arguments": {
|
|
"pnr": "TEST-A1B2C3"
|
|
}
|
|
}
|
|
|
|
// Response: Full booking details
|
|
```
|
|
|
|
**List all bookings in session**
|
|
|
|
```javascript
|
|
{
|
|
"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**
|
|
|
|
```javascript
|
|
{
|
|
"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
|
|
|
|
```javascript
|
|
// 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**
|
|
|
|
```javascript
|
|
{
|
|
"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**
|
|
|
|
```javascript
|
|
{
|
|
"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**
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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:
|
|
```bash
|
|
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**:
|
|
```bash
|
|
# 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):
|
|
```yaml
|
|
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**:
|
|
```nginx
|
|
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)
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```javascript
|
|
// 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**:
|
|
```bash
|
|
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**:
|
|
```bash
|
|
curl -k https://localhost:8080/health | jq
|
|
```
|
|
|
|
**Check rate limits**:
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# First request
|
|
curl -i https://localhost:8080/health
|
|
# Returns:
|
|
# X-RateLimit-Limit: 100
|
|
# X-RateLimit-Remaining: 99
|
|
# X-RateLimit-Reset: 1712486520
|
|
```
|
|
|
|
### Rate Limit Exceeded
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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**:
|
|
```bash
|
|
# 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**:
|
|
```yaml
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: 8080
|
|
scheme: HTTPS
|
|
initialDelaySeconds: 30
|
|
periodSeconds: 10
|
|
```
|
|
|
|
**Prometheus Scraping**:
|
|
```yaml
|
|
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**:
|
|
```bash
|
|
docker-compose logs -f mcp-server | jq
|
|
```
|
|
|
|
**Filter by session**:
|
|
```bash
|
|
docker-compose logs mcp-server | grep "sessionId:abc-123"
|
|
```
|
|
|
|
**Monitor rate limits**:
|
|
```bash
|
|
docker-compose logs mcp-server | grep "RATE_LIMIT_EXCEEDED"
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting (Remote Mode)
|
|
|
|
### Connection Refused
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```bash
|
|
# Increase limit in .env
|
|
RATE_LIMIT_PER_MINUTE=1000
|
|
|
|
# Restart server
|
|
docker-compose restart mcp-server
|
|
```
|
|
|
|
### SSL Certificate Warnings
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# Verify rate limiting kicks in
|
|
ab -n 200 -c 1 https://localhost:8080/health
|
|
|
|
# Should see ~100 successful, ~100 rate limited (429)
|
|
```
|
|
|
|
### Concurrent Sessions
|
|
|
|
```bash
|
|
# 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
|
|
|