fixing jsonSchema validation by using zod
This commit is contained in:
438
src/index.ts
Normal file
438
src/index.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user