feat: complete implementation plan for Mock GDS MCP server
Phase 0 (Research):
- Selected MCP SDK (@modelcontextprotocol/sdk) for protocol compliance
- Chose ioredis for Valkey client (Redis-compatible)
- Minimal dependencies: Pino logging, native test runner
- Docker buildx bake for multi-platform builds
- Embedded mock data with deterministic generation
- PNR format: TEST-{BASE32} for safety
Phase 1 (Design):
- Data model: 8 core entities (Session, PNR, FlightSegment, etc.)
- MCP contracts: 8 tools with JSON schemas
- Quickstart guide with complete workflow examples
- Constitution compliance verified
Technical stack:
- Node.js 20 LTS
- Valkey 8.0+ for persistence
- Docker containers (amd64/arm64)
- Performance: <2s search, 50+ concurrent sessions
Ready for task generation with /speckit.tasks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
394
specs/001-mock-gds-server/data-model.md
Normal file
394
specs/001-mock-gds-server/data-model.md
Normal file
@@ -0,0 +1,394 @@
|
||||
# Data Model: Mock GDS MCP Server
|
||||
|
||||
**Branch**: `001-mock-gds-server` | **Date**: 2026-04-07
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the core data entities, their relationships, validation rules, and state transitions for the Mock GDS MCP server.
|
||||
|
||||
## Core Entities
|
||||
|
||||
### 1. Session
|
||||
|
||||
Represents an MCP client connection with isolated booking context.
|
||||
|
||||
**Fields**:
|
||||
```typescript
|
||||
{
|
||||
id: string; // Unique session identifier (UUID)
|
||||
createdAt: number; // Unix timestamp (ms)
|
||||
expiresAt: number; // Unix timestamp (ms), TTL-based
|
||||
lastActivity: number; // Unix timestamp (ms)
|
||||
bookingCount: number; // Number of bookings created in session
|
||||
searchCount: number; // Number of searches performed
|
||||
}
|
||||
```
|
||||
|
||||
**Storage**: Valkey hash at key `gds:session:{sessionId}`
|
||||
|
||||
**Validation**:
|
||||
- `id` must be valid UUID v4
|
||||
- `expiresAt` must be > `createdAt`
|
||||
- Automatic expiry via Valkey TTL (default 1 hour)
|
||||
|
||||
**State Transitions**:
|
||||
```
|
||||
[Created] → [Active] → [Expired]
|
||||
↓
|
||||
[Ended]
|
||||
```
|
||||
|
||||
### 2. Passenger
|
||||
|
||||
Represents a traveler in a booking.
|
||||
|
||||
**Fields**:
|
||||
```typescript
|
||||
{
|
||||
id: string; // Unique within booking
|
||||
type: enum; // 'adult' | 'child' | 'infant'
|
||||
firstName: string; // Required
|
||||
lastName: string; // Required
|
||||
dateOfBirth: string; // ISO 8601 date (YYYY-MM-DD), optional
|
||||
email: string; // Email address, optional
|
||||
phone: string; // Phone number, optional
|
||||
frequentFlyerNumber: string; // Airline loyalty number, optional
|
||||
}
|
||||
```
|
||||
|
||||
**Validation**:
|
||||
- `firstName`, `lastName` must be 1-50 characters, alphabetic + spaces/hyphens
|
||||
- `email` must match RFC 5322 pattern if provided
|
||||
- `phone` must match E.164 pattern if provided
|
||||
- `type` must be valid enum value
|
||||
|
||||
### 3. FlightSegment
|
||||
|
||||
Represents a single flight leg in an itinerary.
|
||||
|
||||
**Fields**:
|
||||
```typescript
|
||||
{
|
||||
id: string; // Unique segment identifier
|
||||
flightNumber: string; // e.g., "AA123"
|
||||
airlineCode: string; // IATA 2-letter code (e.g., "AA")
|
||||
airlineName: string; // Full airline name
|
||||
originCode: string; // IATA 3-letter airport code (e.g., "JFK")
|
||||
originName: string; // Airport name
|
||||
destinationCode: string; // IATA 3-letter airport code
|
||||
destinationName: string; // Airport name
|
||||
departureTime: string; // ISO 8601 datetime
|
||||
arrivalTime: string; // ISO 8601 datetime
|
||||
duration: number; // Minutes
|
||||
aircraftType: string; // e.g., "Boeing 737-800"
|
||||
cabin: enum; // 'economy' | 'premium_economy' | 'business' | 'first'
|
||||
price: number; // USD cents (e.g., 29900 = $299.00)
|
||||
seatsAvailable: number; // Available seats count
|
||||
bookingClass: string; // Fare class code (e.g., "Y", "J", "F")
|
||||
status: enum; // 'available' | 'sold_out' | 'cancelled'
|
||||
}
|
||||
```
|
||||
|
||||
**Validation**:
|
||||
- `airlineCode` must be valid IATA 2-letter code
|
||||
- `originCode`, `destinationCode` must be valid IATA 3-letter codes
|
||||
- `originCode` ≠ `destinationCode`
|
||||
- `departureTime` < `arrivalTime`
|
||||
- `duration` must match calculated time difference
|
||||
- `seatsAvailable` must be >= 0
|
||||
- `price` must be > 0
|
||||
|
||||
**Business Rules**:
|
||||
- Flight times must be realistic for route (e.g., JFK→LAX ~6 hours)
|
||||
- Prices scale with distance and cabin class
|
||||
- sold_out status when seatsAvailable = 0
|
||||
|
||||
### 4. HotelReservation
|
||||
|
||||
Represents a hotel booking segment.
|
||||
|
||||
**Fields**:
|
||||
```typescript
|
||||
{
|
||||
id: string; // Unique reservation identifier
|
||||
hotelCode: string; // Internal hotel identifier
|
||||
hotelName: string; // Hotel property name
|
||||
chainCode: string; // Hotel chain code (e.g., "MAR" for Marriott)
|
||||
chainName: string; // Hotel chain name
|
||||
address: string; // Full address
|
||||
cityCode: string; // IATA city code (e.g., "LAX")
|
||||
cityName: string; // City name
|
||||
checkInDate: string; // ISO 8601 date (YYYY-MM-DD)
|
||||
checkOutDate: string; // ISO 8601 date (YYYY-MM-DD)
|
||||
nights: number; // Calculated night count
|
||||
roomType: string; // e.g., "Standard King", "Deluxe Suite"
|
||||
rateCode: string; // Rate plan code
|
||||
starRating: number; // 1-5 stars
|
||||
price: number; // USD cents, total for stay
|
||||
pricePerNight: number; // USD cents
|
||||
guestCount: number; // Number of guests
|
||||
amenities: string[]; // List of amenities
|
||||
status: enum; // 'available' | 'sold_out' | 'confirmed' | 'cancelled'
|
||||
}
|
||||
```
|
||||
|
||||
**Validation**:
|
||||
- `checkInDate` < `checkOutDate`
|
||||
- `nights` must equal date difference
|
||||
- `starRating` must be 1-5
|
||||
- `guestCount` must be >= 1
|
||||
- `price` must equal `pricePerNight` * `nights`
|
||||
|
||||
**Business Rules**:
|
||||
- Check-in date must not be in the past
|
||||
- Minimum 1 night stay
|
||||
- Prices vary by star rating and city
|
||||
|
||||
### 5. CarRental
|
||||
|
||||
Represents a car rental segment.
|
||||
|
||||
**Fields**:
|
||||
```typescript
|
||||
{
|
||||
id: string; // Unique rental identifier
|
||||
companyCode: string; // Rental company code (e.g., "ZE" for Hertz)
|
||||
companyName: string; // Company name
|
||||
pickupLocationCode: string; // Airport code or location ID
|
||||
pickupLocationName: string; // Location name
|
||||
dropoffLocationCode: string; // Airport code or location ID
|
||||
dropoffLocationName: string; // Location name
|
||||
pickupDate: string; // ISO 8601 datetime
|
||||
dropoffDate: string; // ISO 8601 datetime
|
||||
vehicleClass: enum; // 'economy' | 'compact' | 'midsize' | 'fullsize' | 'suv' | 'luxury'
|
||||
vehicleModel: string; // e.g., "Toyota Camry or similar"
|
||||
dailyRate: number; // USD cents per day
|
||||
totalPrice: number; // USD cents
|
||||
rentalDays: number; // Number of days
|
||||
mileagePolicy: enum; // 'unlimited' | 'limited'
|
||||
insuranceIncluded: boolean;
|
||||
status: enum; // 'available' | 'confirmed' | 'cancelled'
|
||||
}
|
||||
```
|
||||
|
||||
**Validation**:
|
||||
- `pickupDate` < `dropoffDate`
|
||||
- `rentalDays` must match date calculation
|
||||
- `totalPrice` must equal `dailyRate` * `rentalDays`
|
||||
- `pickupLocationCode` should match airport/city code for traveler's destination
|
||||
|
||||
**Business Rules**:
|
||||
- Same-location dropoff preferred (one-way rentals add surcharge)
|
||||
- Pickup date should align with flight arrival
|
||||
- Dropoff date should align with departure flight
|
||||
|
||||
### 6. PNR (Passenger Name Record)
|
||||
|
||||
Represents a complete booking with multiple service segments.
|
||||
|
||||
**Fields**:
|
||||
```typescript
|
||||
{
|
||||
pnr: string; // Unique booking reference (format: TEST-{BASE32})
|
||||
sessionId: string; // Session that created the booking
|
||||
createdAt: number; // Unix timestamp (ms)
|
||||
lastModified: number; // Unix timestamp (ms)
|
||||
status: enum; // 'pending' | 'confirmed' | 'cancelled'
|
||||
passengers: Passenger[]; // Array of passengers
|
||||
flights: FlightSegment[]; // Array of flight segments
|
||||
hotels: HotelReservation[]; // Array of hotel bookings
|
||||
cars: CarRental[]; // Array of car rentals
|
||||
totalPrice: number; // USD cents, sum of all segments
|
||||
currency: string; // Always "USD" for mock data
|
||||
contactEmail: string; // Primary contact email
|
||||
contactPhone: string; // Primary contact phone
|
||||
}
|
||||
```
|
||||
|
||||
**Storage**: Valkey hash at key `gds:session:{sessionId}:booking:{pnr}`
|
||||
|
||||
**Validation**:
|
||||
- `pnr` must match format `TEST-[A-Z0-9]{6}`
|
||||
- Must have at least one passenger
|
||||
- Must have at least one service (flight, hotel, or car)
|
||||
- `totalPrice` must equal sum of all segment prices
|
||||
- `contactEmail` or `contactPhone` required (at least one)
|
||||
|
||||
**State Transitions**:
|
||||
```
|
||||
[Pending] → [Confirmed] → [Cancelled]
|
||||
↓
|
||||
[Modified] → [Confirmed]
|
||||
```
|
||||
|
||||
**Business Rules**:
|
||||
- Cannot modify after cancellation
|
||||
- Hotel dates must overlap with flight dates
|
||||
- Car pickup should align with flight arrival
|
||||
- Multi-city bookings require connecting flights
|
||||
|
||||
### 7. SearchQuery
|
||||
|
||||
Represents a search request (flights, hotels, or cars).
|
||||
|
||||
**Fields**:
|
||||
```typescript
|
||||
{
|
||||
id: string; // Unique search identifier
|
||||
sessionId: string; // Session performing search
|
||||
type: enum; // 'flight' | 'hotel' | 'car'
|
||||
timestamp: number; // Unix timestamp (ms)
|
||||
parameters: object; // Type-specific search params
|
||||
resultCount: number; // Number of results returned
|
||||
responseTime: number; // Milliseconds to generate results
|
||||
}
|
||||
```
|
||||
|
||||
**Storage**: Ephemeral (not persisted), tracked for statistics only
|
||||
|
||||
### 8. MockDataRecord
|
||||
|
||||
Represents a static mock data entry (airports, airlines, hotels, etc.).
|
||||
|
||||
**Fields**:
|
||||
```typescript
|
||||
{
|
||||
type: enum; // 'airport' | 'airline' | 'hotel' | 'car_company'
|
||||
code: string; // IATA/ICAO code or internal ID
|
||||
name: string; // Full name
|
||||
metadata: object; // Type-specific data (coordinates, address, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
**Storage**: In-memory JavaScript modules, not in Valkey
|
||||
|
||||
## Relationships
|
||||
|
||||
### Session ↔ PNR
|
||||
- **Type**: One-to-Many
|
||||
- **Description**: A session can create multiple bookings
|
||||
- **Key**: `sessionId` in PNR references Session
|
||||
- **Cascade**: PNRs remain accessible after session expires (for retrieval)
|
||||
|
||||
### PNR ↔ Passengers
|
||||
- **Type**: One-to-Many
|
||||
- **Description**: A booking contains multiple passengers
|
||||
- **Embedded**: Passengers stored within PNR document
|
||||
- **Constraint**: Minimum 1 passenger per PNR
|
||||
|
||||
### PNR ↔ FlightSegments
|
||||
- **Type**: One-to-Many
|
||||
- **Description**: A booking can include multiple flight legs
|
||||
- **Embedded**: Flights stored within PNR document
|
||||
- **Ordering**: Flights ordered chronologically
|
||||
|
||||
### PNR ↔ HotelReservations
|
||||
- **Type**: One-to-Many
|
||||
- **Description**: A booking can include multiple hotel stays
|
||||
- **Embedded**: Hotels stored within PNR document
|
||||
|
||||
### PNR ↔ CarRentals
|
||||
- **Type**: One-to-Many
|
||||
- **Description**: A booking can include multiple car rentals
|
||||
- **Embedded**: Cars stored within PNR document
|
||||
|
||||
## Data Validation Rules
|
||||
|
||||
### Cross-Entity Validation
|
||||
|
||||
1. **Date Consistency**:
|
||||
- Hotel check-in must be >= flight arrival date
|
||||
- Hotel check-out must be <= return flight departure date
|
||||
- Car pickup must be >= flight arrival date
|
||||
- Car dropoff must be <= return flight departure date
|
||||
|
||||
2. **Location Consistency**:
|
||||
- Hotel city should match flight destination
|
||||
- Car pickup location should match airport or destination city
|
||||
|
||||
3. **Passenger Consistency**:
|
||||
- All segments in a PNR share the same passenger list
|
||||
- Passenger count must match across segments
|
||||
|
||||
4. **Pricing Integrity**:
|
||||
- PNR total must equal sum of all segment prices
|
||||
- Segment prices must be positive integers
|
||||
|
||||
### Validation Timing
|
||||
|
||||
- **Search-time**: Parameter validation (dates, codes, counts)
|
||||
- **Booking-time**: Business rule validation (date logic, location consistency)
|
||||
- **Retrieval-time**: PNR format validation
|
||||
- **Modification-time**: State transition validation (no modification of cancelled bookings)
|
||||
|
||||
## Mock Data Generation Rules
|
||||
|
||||
### Deterministic Generation
|
||||
|
||||
- Same search inputs → same results (when MOCK_DATA_SEED=fixed)
|
||||
- PNR generation uses session-scoped sequence + timestamp hash
|
||||
- Flight schedules fixed based on route (JFK→LAX always ~6 hours)
|
||||
|
||||
### Realistic Constraints
|
||||
|
||||
- Flight prices: $200-$800 domestic economy, $800-$2000 business, $2500+ first
|
||||
- Hotel prices: $80-$150 budget, $150-$300 midrange, $300-$800 luxury
|
||||
- Car rental: $35-$50 economy, $50-$80 midsize, $100-$150 luxury
|
||||
- Flight duration: Calculated from route distance
|
||||
- Availability: 90% flights available, 10% sold out
|
||||
|
||||
### Data Coverage
|
||||
|
||||
- **Airports**: ~100 major airports (top 50 US + top 50 international)
|
||||
- **Airlines**: ~30 major carriers
|
||||
- **Hotels**: ~50 chains/properties across major cities
|
||||
- **Car Companies**: ~6 major rental companies
|
||||
|
||||
## State Management (Valkey)
|
||||
|
||||
### Key Naming Convention
|
||||
|
||||
```
|
||||
gds:session:{sessionId} # Session metadata
|
||||
gds:session:{sessionId}:booking:{pnr} # Individual booking
|
||||
gds:session:{sessionId}:bookings # Set of all PNRs in session
|
||||
gds:session:{sessionId}:searches # List of search IDs
|
||||
gds:stats:bookings:total # Global booking counter
|
||||
gds:stats:sessions:active # Set of active session IDs
|
||||
```
|
||||
|
||||
### TTL Strategy
|
||||
|
||||
- **Sessions**: 1 hour (configurable via MCP_SESSION_TIMEOUT)
|
||||
- **Bookings**: No expiry (persist beyond session for retrieval)
|
||||
- **Search history**: 10 minutes (ephemeral)
|
||||
|
||||
### Data Serialization
|
||||
|
||||
- **Format**: JSON strings for complex objects
|
||||
- **Encoding**: UTF-8
|
||||
- **Compression**: None (mock data is small)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memory Footprint
|
||||
|
||||
- **Per Session**: ~5KB (metadata only)
|
||||
- **Per Booking**: ~10-30KB (depends on segment count)
|
||||
- **Mock Data**: ~2MB (embedded in code, not in Valkey)
|
||||
|
||||
### Query Patterns
|
||||
|
||||
- **Hot Path**: `GET gds:session:{sessionId}:booking:{pnr}` (booking retrieval)
|
||||
- **Write Path**: `HSET gds:session:{sessionId}:booking:{pnr}` (booking creation)
|
||||
- **Cleanup**: `SCAN` + `DEL` for expired sessions (background job)
|
||||
|
||||
### Indexing
|
||||
|
||||
No secondary indexes required - all queries use primary keys (sessionId, pnr)
|
||||
|
||||
## Next Steps
|
||||
|
||||
Proceed to contract definition (Phase 1 continued):
|
||||
- Define MCP tool schemas in `/contracts/mcp-tools.md`
|
||||
- Create quickstart guide with example usage
|
||||
Reference in New Issue
Block a user