Files
gds-mock-mcp/src/index.ts

439 lines
15 KiB
JavaScript

#!/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 <number> HTTP server port (default: 3000)
--host <address> HTTP server host (default: 127.0.0.1)
--verbose Enable verbose logging (debug level)
--log-level <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();