439 lines
15 KiB
JavaScript
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();
|