#!/usr/bin/env node /** * GDS Mock MCP Server Entry Point * Supports dual transport: stdio (default) or Streamable HTTP (--remote flag) */ import { GDSMockServer } from './server.js'; import { logger, logError } from './utils/logger.js'; import { searchFlights, bookFlight } from './tools/flights.js'; import { retrieveBooking, cancelBooking, listBookings } from './tools/bookings.js'; import { getSessionInfo, clearSession } from './tools/session.js'; import { searchHotels, bookHotel } from './tools/hotels.js'; import { searchCars, bookCar } from './tools/cars.js'; import { createTransport } from './transports/factory.js'; import * as z from 'zod/v4'; /** * Parse CLI arguments */ function parseArgs() { const args = process.argv.slice(2); const options = { port: parseInt(process.env.PORT || '3000', 10), host: process.env.HOST || '127.0.0.1', verbose: false, logLevel: process.env.LOG_LEVEL || 'info' }; for (let i = 0; i < args.length; i++) { switch (args[i]) { case '--port': options.port = parseInt(args[++i], 10); break; case '--host': options.host = args[++i]; break; case '--verbose': options.verbose = true; options.logLevel = 'debug'; break; case '--log-level': options.logLevel = args[++i]; break; case '--help': console.log(` GDS Mock MCP Server - Remote MCP via Streamable HTTP Usage: node src/index.js [options] Options: --port HTTP server port (default: 3000) --host
HTTP server host (default: 127.0.0.1) --verbose Enable verbose logging (debug level) --log-level Set log level (default: info) --help Show this help message Environment Variables: PORT HTTP server port HOST HTTP server host (use 0.0.0.0 for Docker) LOG_LEVEL Logging level VALKEY_HOST Valkey server host (default: localhost) VALKEY_PORT Valkey server port (default: 6379) RATE_LIMIT_MAX Max requests per minute (default: 100) CORS_ORIGINS Allowed CORS origins (comma-separated, default: *) Examples: node src/index.js # Start HTTP server on localhost:3000 node src/index.js --port 8080 # Start on port 8080 node src/index.js --host 0.0.0.0 # Listen on all interfaces node src/index.js --verbose # Enable debug logging Docker: docker compose up -d # Start with Docker Compose `); process.exit(0); break; } } return options; } /** * Main entry point */ async function main() { try { const options = parseArgs(); // Update log level if specified if (options.verbose || options.logLevel) { process.env.LOG_LEVEL = options.logLevel; } logger.info({ options }, 'Starting GDS Mock MCP Server...'); // Create server instance const server = new GDSMockServer(); // Register flight tools server.registerTool( 'searchFlights', 'Search Flights', 'Search for available flights between two airports', z.object({ origin: z.string() .length(3) .regex(/^[A-Z]{3}$/) .describe('Three-letter IATA airport code for departure city (e.g., "JFK", "LAX", "LHR")'), destination: z.string() .length(3) .regex(/^[A-Z]{3}$/) .describe('Three-letter IATA airport code for arrival city (e.g., "LAX", "SFO", "ORD")'), departureDate: z.string() .regex(/^\d{4}-\d{2}-\d{2}$/) .describe('Departure date in ISO 8601 format (YYYY-MM-DD), must be a future date'), passengers: z.object({ adults: z.number() .int() .min(1) .max(9) .optional() .describe('Number of adult passengers (age 18+), default is 1'), children: z.number() .int() .min(0) .max(8) .optional() .describe('Number of child passengers (age 2-17), default is 0'), infants: z.number() .int() .min(0) .max(4) .optional() .describe('Number of infant passengers (under 2 years), default is 0') }).optional().describe('Passenger counts by category (adults, children, infants)'), cabin: z.enum(['economy', 'premium_economy', 'business', 'first']) .optional() .describe('Preferred cabin class: economy (cheapest), premium_economy (extra legroom), business (lie-flat seats), or first (luxury service). Default is economy.') }), searchFlights ); server.registerTool( 'bookFlight', 'Book Flight', 'Create a flight booking with passenger details', z.object({ flightId: z.string() .describe('Unique flight identifier returned from searchFlights operation (e.g., "FL123-ABC-20240115-0800")'), passengers: z.array( z.object({ firstName: z.string() .min(1) .max(50) .describe('Passenger first name as it appears on government-issued ID'), lastName: z.string() .min(1) .max(50) .describe('Passenger last name (surname/family name) as it appears on government-issued ID'), type: z.enum(['adult', 'child', 'infant']) .describe('Passenger type: adult (age 18+), child (age 2-17), or infant (under 2 years)'), dateOfBirth: z.string() .regex(/^\d{4}-\d{2}-\d{2}$/) .optional() .describe('Passenger date of birth in YYYY-MM-DD format, required for international flights'), email: z.string() .email() .optional() .describe('Contact email address for booking confirmation and notifications'), phone: z.string() .optional() .describe('Contact phone number with country code (e.g., "+1-555-0123")') }) ).min(1).describe('Array of passenger information objects, must match passenger count from search') }), bookFlight ); // Register hotel tools server.registerTool( 'searchHotels', 'Search Hotels', 'Search for available hotels in a city', z.object({ cityCode: z.string() .length(3) .regex(/^[A-Z]{3}$/) .describe('Three-letter IATA city/airport code for hotel location (e.g., "NYC", "LAX", "LHR")'), checkInDate: z.string() .regex(/^\d{4}-\d{2}-\d{2}$/) .describe('Hotel check-in date in ISO 8601 format (YYYY-MM-DD), must be a future date'), checkOutDate: z.string() .regex(/^\d{4}-\d{2}-\d{2}$/) .describe('Hotel check-out date in ISO 8601 format (YYYY-MM-DD), must be after check-in date'), guests: z.number() .int() .min(1) .max(10) .optional() .describe('Number of guests staying in the room, default is 1'), rooms: z.number() .int() .min(1) .max(5) .optional() .describe('Number of rooms to book, default is 1') }), searchHotels ); server.registerTool( 'bookHotel', 'Book Hotel', 'Create a hotel booking', z.object({ hotelId: z.string() .describe('Unique hotel identifier returned from searchHotels operation (e.g., "HTL-NYC-001")'), checkInDate: z.string() .regex(/^\d{4}-\d{2}-\d{2}$/) .describe('Check-in date in ISO 8601 format (YYYY-MM-DD), must match search parameters'), checkOutDate: z.string() .regex(/^\d{4}-\d{2}-\d{2}$/) .describe('Check-out date in ISO 8601 format (YYYY-MM-DD), must match search parameters'), guests: z.array( z.object({ firstName: z.string() .min(1) .max(50) .describe('Guest first name as it appears on government-issued ID'), lastName: z.string() .min(1) .max(50) .describe('Guest last name (surname/family name) as it appears on government-issued ID'), email: z.string() .email() .optional() .describe('Contact email address for booking confirmation'), phone: z.string() .optional() .describe('Contact phone number with country code (e.g., "+1-555-0123")') }) ).min(1).describe('Array of guest information objects for the primary guest and additional guests'), rooms: z.number() .int() .min(1) .max(5) .optional() .describe('Number of rooms to book, default is 1') }), bookHotel ); // Register car rental tools server.registerTool( 'searchCars', 'Search Rental Cars', 'Search for available rental cars', z.object({ pickupLocation: z.string() .length(3) .regex(/^[A-Z]{3}$/) .describe('Three-letter IATA airport code for car pickup location (e.g., "LAX", "JFK", "ORD")'), dropoffLocation: z.string() .length(3) .regex(/^[A-Z]{3}$/) .describe('Three-letter IATA airport code for car dropoff location (can be different from pickup for one-way rentals)'), pickupDate: z.string() .regex(/^\d{4}-\d{2}-\d{2}$/) .describe('Car pickup date in ISO 8601 format (YYYY-MM-DD), must be a future date'), dropoffDate: z.string() .regex(/^\d{4}-\d{2}-\d{2}$/) .describe('Car dropoff date in ISO 8601 format (YYYY-MM-DD), must be after pickup date'), driverAge: z.number() .int() .min(18) .max(99) .optional() .describe('Age of primary driver, affects pricing (drivers under 25 may incur young driver fees), default is 30') }), searchCars ); server.registerTool( 'bookCar', 'Book Rental Car', 'Create a car rental booking', z.object({ carId: z.string() .describe('Unique car rental option identifier returned from searchCars operation (e.g., "CAR-LAX-ECO-001")'), pickupDate: z.string() .regex(/^\d{4}-\d{2}-\d{2}$/) .describe('Pickup date in ISO 8601 format (YYYY-MM-DD), must match search parameters'), dropoffDate: z.string() .regex(/^\d{4}-\d{2}-\d{2}$/) .describe('Dropoff date in ISO 8601 format (YYYY-MM-DD), must match search parameters'), driver: z.object({ firstName: z.string() .min(1) .max(50) .describe('Driver first name as it appears on driver\'s license'), lastName: z.string() .min(1) .max(50) .describe('Driver last name (surname/family name) as it appears on driver\'s license'), email: z.string() .email() .describe('Contact email address for booking confirmation and rental instructions'), phone: z.string() .describe('Contact phone number with country code (e.g., "+1-555-0123")'), licenseNumber: z.string() .optional() .describe('Driver\'s license number, may be required for pickup'), age: z.number() .int() .min(18) .max(99) .optional() .describe('Driver age, must match driverAge from search if specified') }).describe('Primary driver information for the car rental') }), bookCar ); // Register booking management tools server.registerTool( 'retrieveBooking', 'Retrieve Booking', 'Retrieve booking details by PNR', z.object({ pnr: z.string() .regex(/^TEST-[A-Z0-9]{6}$/) .describe('Passenger Name Record (PNR) - unique 6-character booking reference code with TEST- prefix (e.g., "TEST-ABC123")') }), retrieveBooking ); server.registerTool( 'cancelBooking', 'Cancel Booking', 'Cancel an existing booking', z.object({ pnr: z.string() .regex(/^TEST-[A-Z0-9]{6}$/) .describe('Passenger Name Record (PNR) of the booking to cancel - must be an active booking (e.g., "TEST-ABC123")') }), cancelBooking ); server.registerTool( 'listBookings', 'List Bookings', 'List all bookings in current session', z.object({ limit: z.number() .int() .min(1) .max(100) .optional() .describe('Maximum number of bookings to return per page, default is 10, maximum is 100'), offset: z.number() .int() .min(0) .optional() .describe('Number of bookings to skip for pagination, default is 0 (use with limit for paging through results)') }), listBookings ); // Register session management tools server.registerTool( 'getSessionInfo', 'Get Session Info', 'Get current session information and statistics', z.object({}).describe('No input parameters required - returns information about the current MCP session'), getSessionInfo ); server.registerTool( 'clearSession', 'Clear Session', 'Clear all bookings from current session', z.object({ confirm: z.boolean() .optional() .describe('Confirmation flag - must be set to true to proceed with clearing all session bookings (destructive operation)') }), clearSession ); // Initialize storage (needed by all tools) await server.initStorage(); // Create transport (stdio or HTTP) // This connects the server to the transport for both modes const { shutdown: transportShutdown } = await createTransport(server, options); // Handle graceful shutdown const shutdown = async (signal) => { logger.info({ signal }, 'Received shutdown signal'); try { if (transportShutdown) { await transportShutdown(); } await server.stop(); process.exit(0); } catch (error) { logError(error, { context: 'shutdown' }); process.exit(1); } }; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); // Handle uncaught errors process.on('uncaughtException', (error) => { logError(error, { context: 'uncaughtException' }); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { logError(new Error(String(reason)), { context: 'unhandledRejection', promise: promise.toString() }); process.exit(1); }); } catch (error) { logError(error, { context: 'main' }); process.exit(1); } } // Start the server main();