Merge branch '001-mock-gds-server'

This commit is contained in:
2026-04-11 22:35:55 -05:00
59 changed files with 14813 additions and 0 deletions

42
.env.example Normal file
View File

@@ -0,0 +1,42 @@
# ==============================================
# MCP Server Configuration
# ==============================================
# Logging configuration
LOG_LEVEL=info # Options: silent, error, warn, info, debug, trace
NODE_ENV=production
# ==============================================
# Valkey (Redis-compatible) Configuration
# ==============================================
VALKEY_HOST=localhost
VALKEY_PORT=6379
VALKEY_PASSWORD=
VALKEY_DB=0
# ==============================================
# Session Management
# ==============================================
# Session timeout in seconds (default: 3600 = 1 hour)
MCP_SESSION_TIMEOUT=3600
# ==============================================
# Mock Data Configuration
# ==============================================
# Data generation seed
# Options: random (default), fixed (deterministic), demo (curated demo data)
MOCK_DATA_SEED=random
# Response delay simulation (milliseconds)
# Set to 0 to disable artificial delays
MOCK_RESPONSE_DELAY=0
# ==============================================
# Docker-specific Configuration
# ==============================================
# When running in Docker, use service name as host
# VALKEY_HOST=valkey

View File

@@ -0,0 +1,46 @@
---
name: gds-mock-mcp Docker Build
on:
push:
branches: [main]
tags: [v*]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Installs QEMU binaries for multi-platform emulation.
uses: docker/setup-qemu-action@v4
- name: Generates image tags and annotations from Git information.
id: meta
uses: docker/metadata-action@v6
with:
images: registry.mortons.site/gds-mock-mcp
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=ref,event=branch
type=ref,event=pr
type=sha
- name: Login to registry
if: ${{ gitea.event_name != 'pull_request' }}
uses: docker/login-action@v4
with:
registry: registry.mortons.site
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Configures a Docker Buildx builder instance.
uses: docker/setup-buildx-action@v4
- name: Builds and pushes the Docker image using the Buildx environment.
uses: docker/bake-action@v7
with:
push: ${{ gitea.event_name != 'pull_request' }}
files: |
./docker/docker-bake.hcl
cwd://${{ steps.meta.outputs.bake-file }}
targets: build

238
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,238 @@
# Copilot Instructions: Speckit Framework
This repository contains a **spec-driven development framework** (Speckit) that provides structured workflows for feature specification, planning, and implementation through custom Copilot agents.
## Running Scripts
All workflow scripts are located in `.specify/scripts/bash/` and must be run from the repository root:
```bash
# Check prerequisites for current feature branch
./.specify/scripts/bash/check-prerequisites.sh --json
# Create a new feature branch (auto-detects next sequential number)
./.specify/scripts/bash/create-new-feature.sh --json --short-name "feature-name" "Feature description"
# For timestamp-based branch naming (check .specify/init-options.json first)
./.specify/scripts/bash/create-new-feature.sh --json --timestamp --short-name "feature-name" "Feature description"
```
**Important**:
- VS Code is configured to auto-approve scripts in `.specify/scripts/bash/` (see `.vscode/settings.json`)
- Always use `--json` flag for parseable output
- Quote arguments with special characters properly: `'I'\''m text'` or `"I'm text"`
- Scripts use `set -e` - they exit on first error
## Architecture
### Workflow Structure
Speckit follows a **phase-based specification workflow**:
1. **Specification** (`/speckit.specify`) → Creates `spec.md` (what/why, no implementation)
2. **Planning** (`/speckit.plan`) → Creates `plan.md`, `research.md`, `data-model.md`, contracts
3. **Task Generation** (`/speckit.tasks`) → Creates `tasks.md` (ordered, dependency-aware)
4. **Analysis** (`/speckit.analyze`) → Cross-validates artifacts for consistency
5. **Implementation** (`/speckit.implement`) → Executes tasks from `tasks.md`
Additional commands:
- `/speckit.clarify` - Interactive clarification of underspecified requirements
- `/speckit.checklist` - Generate custom checklists for features
- `/speckit.constitution` - Manage project principles and standards
- `/speckit.taskstoissues` - Convert tasks.md into GitHub issues
### Directory Layout
```
.github/
├── agents/ # Agent definitions (.agent.md files)
├── prompts/ # Agent prompt templates (.prompt.md files)
└── copilot-instructions.md
.specify/
├── init-options.json # Branch numbering mode (sequential/timestamp)
├── integration.json # Integration config (copilot, speckit version)
├── memory/
│ └── constitution.md # Project constitution (template, not yet filled)
├── templates/ # Templates for spec, plan, tasks, etc.
├── scripts/
│ └── bash/ # Workflow automation scripts
└── integrations/
└── copilot/ # Copilot-specific integration scripts
specs/ # Created when first feature branch is made
└── ###-feature-name/ # Feature directories (auto-numbered or timestamped)
├── spec.md # Feature specification (what/why)
├── plan.md # Implementation plan (how)
├── tasks.md # Task breakdown
├── research.md # Technical research
├── data-model.md # Data structures
├── quickstart.md # Usage examples
├── contracts/ # API/interface contracts
└── checklists/ # Quality validation checklists
```
### Feature Branch Workflow
- Branch naming: `###-short-feature-name` (e.g., `001-user-auth`, `20260407-143052-user-auth`)
- Numbering mode stored in `.specify/init-options.json` (`branch_numbering`: `"sequential"` or `"timestamp"`)
- Each feature gets a directory under `specs/###-feature-name/`
- Scripts validate you're on a properly named feature branch before running
## Custom Copilot Agents
All agents are defined in `.github/agents/*.agent.md` with prompts in `.github/prompts/*.prompt.md`.
**Key Agent Patterns**:
1. **Extension Hooks**: Agents check for `.specify/extensions.yml` before/after execution
- Optional hooks: Present to user with description
- Mandatory hooks: Auto-execute with `EXECUTE_COMMAND`
- Hook conditions are NOT evaluated by agents (delegated to HookExecutor)
2. **Constitution Authority**: `.specify/memory/constitution.md` defines non-negotiable principles
- Constitution violations are always CRITICAL
- Agents must validate against constitution rules
- Constitution changes require separate, explicit workflow
3. **Progressive Disclosure**: Agents load minimal context, avoid token waste
- Build semantic models internally (don't dump raw artifacts)
- Limit findings to high-signal items (e.g., max 50 in analysis)
- Use stable identifiers (kebab-case slugs, not generic IDs)
4. **Read-Only Analysis**: `/speckit.analyze` NEVER modifies files
- Outputs structured report
- Offers remediation plan (requires user approval)
- Validates cross-artifact consistency
## Key Conventions
### Specification Quality
Specs must be **technology-agnostic** and **testable**:
- ✅ Focus on WHAT and WHY (user needs, business value)
- ❌ No implementation details (frameworks, APIs, code structure)
- ✅ Success criteria must be measurable and user-focused
- ❌ No technical metrics (API latency, database TPS)
- ✅ Maximum 3 `[NEEDS CLARIFICATION]` markers per spec
- ✅ Requirements must have clear acceptance criteria
**Good Success Criteria**:
- "Users complete checkout in under 3 minutes"
- "System supports 10,000 concurrent users"
- "95% of searches return results in under 1 second"
**Bad Success Criteria** (too technical):
- "API response time under 200ms" → Use "Users see results instantly"
- "Database handles 1000 TPS" → Use user-facing metric
- "Redis cache hit rate above 80%" → Technology-specific
### Planning Phase
Plans (`plan.md`) are technical but defer implementation until tasks:
- Architecture/stack decisions documented
- Constitution compliance gates checked before Phase 0
- Technical research informs technology choices
- Data models, contracts, and quickstarts created
- Performance goals and constraints specified (domain-specific)
### Task Generation
Tasks (`tasks.md`) must be:
- **Ordered by dependencies** - foundation before integration
- **Marked for parallelization** - `[P]` for independent tasks
- **Phase-grouped** - logical implementation stages
- **File-path specific** - reference exact files to create/modify
- **Testable** - clear completion criteria
### Script Conventions
Scripts in `.specify/scripts/bash/`:
1. **common.sh** - Shared functions for feature detection
2. **check-prerequisites.sh** - Validates current context
- `--json` - Structured output
- `--require-tasks` - Enforce tasks.md exists
- `--include-tasks` - Add tasks.md to available docs
- `--paths-only` - Skip validation, just return paths
3. **create-new-feature.sh** - Initialize feature branches
- Auto-detects next sequential number (don't pass `--number`)
- Supports `--timestamp` for timestamp-based naming
- Always use `--short-name "name"` for branch suffix
## Agent Handoffs
Agents define suggested next steps via `handoffs` in frontmatter:
```yaml
---
description: Agent purpose
handoffs:
- label: Build Technical Plan
agent: speckit.plan
prompt: Create a plan for the spec. I am building with...
- label: Clarify Spec Requirements
agent: speckit.clarify
prompt: Clarify specification requirements
send: true
---
```
When you see handoff suggestions, route the user to the appropriate next command.
## Integration Notes
- **AI**: Configured for GitHub Copilot (`.specify/init-options.json`)
- **Version**: Speckit v0.5.1.dev0
- **Context Updates**: `.specify/integrations/copilot/scripts/update-context.sh` syncs agent context
## Common Workflows
### Starting a New Feature
1. User provides feature description
2. Run `/speckit.specify` → Creates spec.md with quality checklist
3. Validate spec passes quality gates (no CRITICAL issues)
4. Run `/speckit.plan` → Creates technical plan
5. Run `/speckit.tasks` → Generates ordered task list
6. Optional: Run `/speckit.analyze` → Cross-validate artifacts
7. Run `/speckit.implement` → Execute implementation
### Validating Specifications
- Specs auto-validate against checklist in `checklists/requirements.md`
- Max 3 validation iterations before reporting remaining issues
- Clarifications presented as multi-choice tables (max 3 questions)
- Markdown tables must be properly formatted (aligned pipes, spaced cells)
### Constitution Management
- Template stored at `.specify/memory/constitution.md`
- Defines MUST/SHOULD principles for the project
- Use `/speckit.constitution` to create/update from principles
- All specs and plans validated against constitution
- Constitution violations block implementation
## When Working with This Repository
1. **Understand the meta-nature**: This is a framework FOR building projects, not a project itself
2. **Respect the workflow phases**: Specification → Planning → Tasks → Implementation
3. **Validate branch context**: Most commands require being on a feature branch
4. **Use JSON output**: Always add `--json` to scripts for parseable results
5. **Check hooks**: Extensions can inject pre/post workflow hooks
6. **Don't hallucinate**: If a file doesn't exist, report it accurately
7. **Token efficiency**: Load minimal context, produce high-signal findings
## Active Technologies
- Node.js 20 LTS (current stable) + Minimal libraries - MCP SDK (@modelcontextprotocol/sdk), Valkey client (ioredis), Docker buildx for multi-platform builds (001-mock-gds-server)
- Valkey 8.0+ (Redis-compatible in-memory store with persistence) (001-mock-gds-server)
- Node.js 20 LTS (ES modules) + @modelcontextprotocol/sdk ^1.0.4 (MCP protocol), ioredis ^5.4.1 (Valkey client), pino ^9.5.0 (structured logging), HTTP/2 server library (TBD in research phase) (001-mock-gds-server)
- Valkey (Redis-compatible) for session state, PNR storage with TTL, rate limiting counters (001-mock-gds-server)
- Valkey 8.0+ (Redis-compatible in-memory store with persistence for PNRs and sessions) (001-mock-gds-server)
- Node.js 20 LTS with TypeScript 6.0.2 + @modelcontextprotocol/sdk ^1.0.4 (MCP protocol), @modelcontextprotocol/server ^2.0.0-alpha.2 (McpServer), @modelcontextprotocol/express ^2.0.0-alpha.2 (HTTP transport), ioredis ^5.4.1 (Valkey client), Express ^5.2.1, pino ^9.5.0 (structured logging) (001-mock-gds-server)
## Recent Changes
- 001-mock-gds-server: Added Node.js 20 LTS (current stable) + Minimal libraries - MCP SDK (@modelcontextprotocol/sdk), Valkey client (ioredis), Docker buildx for multi-platform builds

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Node.js
node_modules/
npm-debug.log
yarn-error.log
pnpm-debug.log
lerna-debug.log
.npm
.eslintcache
# Build outputs
dist/
build/
*.tsbuildinfo
# Environment
.env
.env*.local
.env.production
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
Thumbs.db
# Test coverage
coverage/
.nyc_output/
# Logs
logs/
*.log
# Docker
.dockerignore
# Temporary files
tmp/
temp/
*.tmp

10
.prettierignore Normal file
View File

@@ -0,0 +1,10 @@
# Prettier ignore patterns
node_modules/
dist/
build/
coverage/
package-lock.json
yarn.lock
pnpm-lock.yaml
*.min.js
.git/

9
.prettierrc Normal file
View File

@@ -0,0 +1,9 @@
{
"singleQuote": true,
"trailingComma": "none",
"tabWidth": 2,
"semi": true,
"printWidth": 100,
"arrowParens": "always",
"endOfLine": "lf"
}

View File

@@ -0,0 +1,9 @@
{
"ai": "copilot",
"branch_numbering": "sequential",
"here": true,
"integration": "copilot",
"preset": null,
"script": "sh",
"speckit_version": "0.5.1.dev0"
}

102
CHANGELOG.md Normal file
View File

@@ -0,0 +1,102 @@
# Changelog
All notable changes to the GDS Mock MCP Server will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.0] - 2026-04-08
### Added
- Initial release of GDS Mock MCP Server
- **Flight Search and Booking** (User Story 1 - MVP)
- `searchFlights` tool: Search for flights between airports with realistic pricing
- `bookFlight` tool: Create flight bookings with passenger details
- Mock data: 100+ airports, 30+ airlines, deterministic flight generation
- Support for economy, business, and first class cabins
- Realistic pricing ($200-$2500+ based on route and class)
- **Session Management** (User Story 4)
- Concurrent session support with UUID v4 session IDs
- Session isolation: bookings scoped to sessions with zero data leakage
- Session TTL management (1 hour default, configurable)
- Activity tracking and automatic cleanup
- **Remote Access** (User Story 6)
- **MCP 2025-11-25 Streamable HTTP transport** (HTTP/1.1 + SSE)
- Protocol version validation middleware (strict 2025-11-25)
- IP-based rate limiting (100 req/min default, Valkey-backed)
- CORS middleware with permissive wildcard policy
- Health check endpoint (`/health`)
- CLI arguments: `--remote`, `--port`, `--host`, `--verbose`, `--log-level`
- Dual transport support: stdio (default) or HTTP
- SSE polling pattern with Last-Event-ID resumption
- Example remote client demonstrating SSE connection
- Nginx reverse proxy configuration template
- **Hotel Search and Bundling** (User Story 2)
- `searchHotels` tool: Search hotels by city with pricing tiers
- `bookHotel` tool: Create hotel bookings and bundle with flights
- Mock data: 50+ properties across major cities (3-5 star ratings)
- Realistic pricing: budget ($80-$150), midrange ($150-$300), luxury ($300-$800)
- Multi-service bundling: combine hotels with flights under single PNR
- **Car Rental Integration** (User Story 3)
- `searchCars` tool: Search car rentals by pickup/dropoff location
- `bookCar` tool: Create car rental bookings and bundle with flights/hotels
- Mock data: 8 rental companies, 8 vehicle categories
- Complete travel package support: flights + hotels + cars under one PNR
- **Booking Management Tools**
- `retrieveBooking` tool: Fetch booking details by PNR
- `cancelBooking` tool: Cancel bookings with status validation
- `listBookings` tool: List all bookings in current session
- `getSessionInfo` tool: View session metadata
- `clearSession` tool: Clear session data
- **Infrastructure**
- Valkey 8.0+ integration for session and PNR storage
- Structured logging with Pino
- MCP error codes and error handling
- JSON schema validation
- PNR generation with TEST- prefix (base32 encoding)
- Docker Compose configuration for Valkey
- Multi-stage Dockerfile with non-root user
- ESLint and Prettier configuration
### Technical Details
- **Node.js 20 LTS** with ES modules
- **@modelcontextprotocol/sdk v1.0.4+** for MCP protocol
- **ioredis v5.4.1** for Valkey/Redis client
- **Pino v9.5.0** for structured logging
- **Express v4.18+** for HTTP middleware
- **express-rate-limit v7.1+** for IP-based rate limiting
- **CORS v2.8+** for cross-origin support
### Security
- PNRs prefixed with TEST- to prevent confusion with production
- No real external API calls (100% self-contained)
- Rate limiting on remote access (100 req/min per IP)
- Localhost binding by default (127.0.0.1) per MCP security
- Non-root Docker user
- Session isolation with cryptographically secure UUIDs
### Documentation
- Comprehensive README with installation and usage instructions
- SAFETY_DISCLAIMER.md emphasizing testing/demo purpose only
- Inline JSDoc comments for all public functions
- Example remote client with SSE demonstration
- Nginx configuration template for optional HTTP/2 upgrade
## [Unreleased]
### Planned
- Additional search filters (airline preference, hotel amenities, car features)
- Enhanced error messages with suggested corrections
- Metrics and monitoring endpoints
- Load testing and performance optimization
- Additional mock data for global coverage
---
**Note**: This is a mock server for testing and demonstration purposes only. All data is simulated. No real travel bookings are created.

261
QUICKSTART.md Normal file
View File

@@ -0,0 +1,261 @@
# Quick Start Guide - GDS Mock MCP Server
## Prerequisites
- Node.js 20 LTS
- Docker and Docker Compose (recommended)
- Valkey 8.0+ or Redis
## Running the Server
### Option 1: Docker Compose (Recommended)
```bash
# Start all services (GDS server + Valkey)
docker compose up -d
# Check status
docker compose ps
# View logs
docker compose logs -f gds-mock-mcp
# Stop services
docker compose down
```
### Option 2: Local Development
```bash
# Start Valkey
docker run -d --name valkey -p 6379:6379 valkey/valkey:8-alpine
# Start the HTTP server
node src/index.js
# Server will listen on http://127.0.0.1:3000
```
## Testing the Server
### Health Check
```bash
curl http://localhost:3000/health
```
Expected response:
```json
{
"status": "healthy",
"service": "gds-mock-mcp",
"uptime": 10.5,
"timestamp": "2026-04-08T03:00:00.000Z"
}
```
### MCP Initialize
First, initialize an MCP session to get a session ID:
```bash
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-protocol-version: 2025-11-25" \
-d '{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
}
}
}'
```
The response will include a session ID in the `mcp-session-id` header. Extract this for subsequent requests.
Example response:
```
< mcp-session-id: 550e8400-e29b-41d4-a716-446655440000
data: {"jsonrpc":"2.0","result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{}},"serverInfo":{"name":"gds-mock-mcp","version":"0.1.0"}},"id":1}
```
### List Available Tools
Use the session ID from the initialize response:
```bash
# Replace SESSION_ID with the actual session ID from initialization
SESSION_ID="550e8400-e29b-41d4-a716-446655440000"
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-protocol-version: 2025-11-25" \
-H "mcp-session-id: $SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 2,
"params": {}
}'
```
### Search for Flights
```bash
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-protocol-version: 2025-11-25" \
-H "mcp-session-id: $SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 3,
"params": {
"name": "searchFlights",
"arguments": {
"origin": "LAX",
"destination": "JFK",
"departureDate": "2026-05-15",
"passengers": 1
}
}
}'
```
## Available Tools
The server provides 11 MCP tools:
### Flight Operations
- `searchFlights` - Search for available flights
- `bookFlight` - Book a flight with passenger details
### Hotel Operations
- `searchHotels` - Search for hotels by city
- `bookHotel` - Book a hotel room
### Car Rental Operations
- `searchCars` - Search for rental cars
- `bookCar` - Book a rental car
### Booking Management
- `retrieveBooking` - Get booking details by PNR
- `cancelBooking` - Cancel an existing booking
- `listBookings` - List all bookings in current session
### Session Management
- `getSessionInfo` - Get current session information
- `clearSession` - Clear all bookings from session
## Environment Variables
### Server Configuration
- `PORT` - HTTP server port (default: `3000`)
- `HOST` - HTTP server host (default: `127.0.0.1`, use `0.0.0.0` for Docker)
- `LOG_LEVEL` - Logging level (`trace`, `debug`, `info`, `warn`, `error`, default: `info`)
- `NODE_ENV` - Environment (`development` or `production`, default: `development`)
### Valkey/Redis Configuration
- `VALKEY_HOST` - Valkey host (default: `localhost`)
- `VALKEY_PORT` - Valkey port (default: `6379`)
- `VALKEY_PASSWORD` - Valkey password (optional)
- `VALKEY_DB` - Valkey database number (default: `0`)
### Rate Limiting & CORS
- `RATE_LIMIT_MAX` - Max requests per minute (default: `100`)
- `CORS_ORIGINS` - Allowed CORS origins (comma-separated, default: `*`)
## Command Line Arguments
```bash
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 (sets LOG_LEVEL=debug)
--log-level <level> Set log level (trace|debug|info|warn|error)
--help Show help message
```
## Example Usage
### Start HTTP server on custom port
```bash
node src/index.js --port 8080 --host 0.0.0.0
```
### Start with debug logging
```bash
node src/index.js --verbose
```
### Docker with custom port
```bash
PORT=8080 docker compose up -d
```
## Troubleshooting
### Server won't start
1. Check Valkey is running: `docker ps | grep valkey`
2. Check port is available: `lsof -i :3000`
3. Check logs: `docker compose logs gds-mock-mcp`
### "Connection refused" errors
- Ensure Valkey is running on port 6379
- Check `VALKEY_HOST` environment variable
### "Empty reply from server"
- This error was fixed in the latest version
- Ensure you're using the updated code
### Rate limit errors (429)
- Default: 100 requests per minute per IP
- Wait 60 seconds or adjust rate limit in `src/middleware/rate-limit.js`
## Development
### Install dependencies
```bash
npm install
```
### Run linter
```bash
npm run lint
```
### Build Docker image
```bash
npm run docker:build
```
## Safety Notes
⚠️ **This is a MOCK server for testing and demonstration purposes only.**
- All PNRs are prefixed with `TEST-` to prevent production confusion
- Uses in-memory Valkey for session storage (data is not persistent)
- CORS is configured with wildcard (`*`) for development
- Not suitable for production without security enhancements
For production use, implement:
- Real GDS API integrations (Amadeus, Sabre, Travelport)
- Proper authentication and authorization
- TLS/SSL encryption
- Persistent database storage
- Restricted CORS policy
- Rate limiting based on authenticated users
---
**Version**: 0.1.0
**MCP Protocol**: 2025-11-25
**Last Updated**: 2026-04-08

509
README.md Normal file
View File

@@ -0,0 +1,509 @@
# Mock GDS MCP Server
A **Remote MCP** (Model Context Protocol) server that simulates Global Distribution System (GDS) functionality for testing and demonstration purposes. Uses **MCP 2025-11-25 Streamable HTTP** transport (HTTP/1.1 + Server-Sent Events).
## ⚠️ IMPORTANT SAFETY NOTICE
**THIS IS A MOCK SERVER FOR TESTING AND DEMONSTRATION ONLY**
- ❌ Does NOT connect to real GDS systems
- ❌ Does NOT process real bookings or transactions
- ❌ Does NOT charge credit cards or process payments
- ✅ All bookings are simulated with `TEST-` prefixed PNRs
- ✅ Safe for development, testing, and training environments
See [SAFETY_DISCLAIMER.md](./SAFETY_DISCLAIMER.md) for complete details.
---
## Features
- **Remote MCP Access**: HTTP/1.1 + SSE transport per MCP 2025-11-25 specification
- **Flight Operations**: Search flights, create bookings, retrieve confirmations
- **Hotel Bookings**: Search and book hotels with realistic pricing
- **Car Rentals**: Search and book rental cars from major providers
- **Multi-Service Bundling**: Combined bookings under single PNR
- **Session Management**: Isolated sessions with UUID v4 IDs and TTL
- **Rate Limiting**: 100 requests/minute per IP (Valkey-backed)
- **Realistic Mock Data**: Valid IATA codes, realistic pricing, professional presentation
- **Docker Ready**: Multi-platform container images (amd64/arm64)
## Quick Start
### Prerequisites
- Node.js 20+ LTS
- Docker and Docker Compose (recommended)
- Valkey 8.0+ or Redis (for storage backend)
### Installation
#### Option 1: Docker Deployment (Recommended)
```bash
# Clone repository
git clone <repository-url>
cd gds-mock-mcp
# Build and start all services
docker compose up -d
# Verify health
curl http://localhost:3000/health
```
#### Option 2: Local Development
```bash
# Install dependencies
npm install
# Start Valkey
docker run -d --name valkey -p 6379:6379 valkey/valkey:8-alpine
# Start HTTP server
node src/index.js
```
### Testing the Server
```bash
# Check health
curl http://localhost:3000/health
# Initialize MCP session
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-protocol-version: 2025-11-25" \
-d '{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0.0"}
}
}'
```
**Environment Variables:**
You can customize deployment with environment variables:
```bash
# Custom port
PORT=8080 docker compose up -d
# Change log level
LOG_LEVEL=debug docker compose up -d
# Bind to specific host
HOST=0.0.0.0 docker compose up -d
```
## MCP Tools
The server provides **11 MCP tools** for travel booking operations:
### Flight Operations
- `searchFlights` - Search for available flights
- `bookFlight` - Book a flight with passenger details
### Hotel Operations
- `searchHotels` - Search for hotels by city
- `bookHotel` - Book a hotel room
### Car Rental Operations
- `searchCars` - Search for rental cars
- `bookCar` - Book a rental car
### Booking Management
- `retrieveBooking` - Get booking details by PNR
- `cancelBooking` - Cancel an existing booking
- `listBookings` - List all bookings in session
### Session Management
- `getSessionInfo` - Get current session information
- `clearSession` - Clear all bookings from session
## Usage Examples
### Health Check
```bash
curl http://localhost:3000/health
```
### 1. Initialize MCP Session
First, initialize to get a session ID:
```bash
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-protocol-version: 2025-11-25" \
-d '{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0.0"}
}
}'
```
**Extract the session ID from the response headers:**
```
< mcp-session-id: 550e8400-e29b-41d4-a716-446655440000
```
### 2. List Available Tools
```bash
# Use the session ID from step 1
SESSION_ID="550e8400-e29b-41d4-a716-446655440000"
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-protocol-version: 2025-11-25" \
-H "mcp-session-id: $SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 2,
"params": {}
}'
```
### 3. Search Flights
```bash
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-protocol-version: 2025-11-25" \
-H "mcp-session-id: $SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 3,
"params": {
"name": "searchFlights",
"arguments": {
"origin": "JFK",
"destination": "LAX",
"departureDate": "2026-05-01",
"passengers": 1
}
}
}'
```
## Architecture
### Remote MCP Transport
- **Protocol**: MCP 2025-11-25 Streamable HTTP
- **Transport**: HTTP/1.1 + Server-Sent Events (SSE)
- **Endpoint**: `/mcp` for all MCP operations
- **Session Management**: UUID v4 session IDs with 1-hour TTL
### Production Deployment
For production use, consider using Nginx as a reverse proxy to add:
- TLS/SSL encryption (HTTPS)
- HTTP/2 support for clients
- Additional rate limiting
- Request logging
See `docker/nginx.conf.example` for a sample Nginx configuration.
## Usage
### MCP Tool: searchFlights
Search for available flights between two airports.
**Input:**
```json
{
"origin": "JFK",
"destination": "LAX",
"departureDate": "2026-06-15",
"passengers": { "adults": 2 },
"cabin": "economy"
}
```
**Output:**
```json
{
"search": { "origin": "JFK", "destination": "LAX", "departureDate": "2026-06-15" },
"results": [
{
"id": "flight-0-1234567890",
"flightNumber": "AA123",
"airlineCode": "AA",
"airlineName": "American Airlines",
"originCode": "JFK",
"destinationCode": "LAX",
"departureTime": "2026-06-15T08:00:00",
"arrivalTime": "2026-06-15T11:30:00",
"duration": 390,
"cabin": "economy",
"price": 29900,
"seatsAvailable": 15,
"bookingClass": "Y",
"status": "available"
}
],
"resultCount": 5
}
```
### MCP Tool: bookFlight
Create a flight booking.
**Input:**
```json
{
"flightId": "flight-0-1234567890",
"passengers": [
{
"firstName": "John",
"lastName": "Doe",
"type": "adult",
"email": "john.doe@example.com"
}
],
"contactEmail": "john.doe@example.com",
"flightDetails": { /* full flight object from search */ }
}
```
**Output:**
```json
{
"success": true,
"pnr": "TEST-A1B2C3",
"status": "confirmed",
"message": "Flight booked successfully. PNR: TEST-A1B2C3",
"booking": {
"pnr": "TEST-A1B2C3",
"passengers": [...],
"flights": [...],
"totalPrice": 29900,
"currency": "USD"
}
}
```
### MCP Tool: retrieveBooking
Retrieve booking details by PNR.
**Input:**
```json
{
"pnr": "TEST-A1B2C3"
}
```
### MCP Tool: cancelBooking
Cancel an existing booking.
**Input:**
```json
{
"pnr": "TEST-A1B2C3"
}
```
### MCP Tool: listBookings
List all bookings in current session.
**Input:**
```json
{
"limit": 10,
"offset": 0
}
```
## Configuration
Configuration is managed via environment variables. Copy `.env.example` to `.env` and customize:
```bash
cp .env.example .env
```
### Key Configuration Options
| Variable | Default | Description |
|----------|---------|-------------|
| `LOG_LEVEL` | `info` | Logging verbosity (silent, error, warn, info, debug, trace) |
| `VALKEY_HOST` | `localhost` | Valkey/Redis host |
| `VALKEY_PORT` | `6379` | Valkey/Redis port |
| `MCP_SESSION_TIMEOUT` | `3600` | Session timeout in seconds (1 hour) |
| `MOCK_DATA_SEED` | `random` | Data generation mode (random, fixed, demo) |
## Architecture
```
┌─────────────────┐
│ MCP Client │
│ (CLI/Agent) │
└────────┬────────┘
│ stdio
┌─────────────────┐
│ GDS Mock MCP │
│ Server │
│ (Node.js 20) │
└────────┬────────┘
┌─────────────────┐
│ Valkey 8.0+ │
│ (Storage) │
└─────────────────┘
```
## Project Structure
```
├── src/
│ ├── index.js # Entry point
│ ├── server.js # MCP server
│ ├── tools/ # MCP tool handlers
│ │ ├── flights.js # Flight operations
│ │ └── bookings.js # Booking management
│ ├── data/ # Mock data generators
│ │ ├── airports.js # Airport data (100+ airports)
│ │ ├── airlines.js # Airline data (30+ carriers)
│ │ ├── flights.js # Flight generator
│ │ └── pnr.js # PNR generation
│ ├── session/ # Session management
│ │ ├── manager.js # Session lifecycle
│ │ └── storage.js # Valkey client
│ ├── validation/ # Input validation
│ └── utils/ # Utilities (logging, errors)
├── docker/
│ ├── Dockerfile # Multi-stage build
│ └── docker-bake.hcl # Multi-platform config
├── tests/ # Test suite
└── specs/ # Design documents
```
## Development
### Running Tests
```bash
npm test # All tests
npm run test:unit # Unit tests only
npm run test:integration # Integration tests only
```
### Code Quality
```bash
npm run lint # Check code style
npm run lint:fix # Fix code style issues
npm run format # Format with Prettier
npm run format:check # Check formatting
```
### Building Docker Images
```bash
# Build for all platforms
npm run docker:build
# Or use buildx directly
docker buildx bake -f docker/docker-bake.hcl
# Build for specific platform
docker buildx bake -f docker/docker-bake.hcl amd64
docker buildx bake -f docker/docker-bake.hcl arm64
```
## Troubleshooting
### Server Won't Start
**Problem:** Server exits immediately after starting
**Solution:**
1. Check Valkey is running: `docker-compose ps valkey`
2. Verify connection: `docker-compose logs valkey`
3. Check environment variables: `cat .env`
### Connection Refused
**Problem:** `ECONNREFUSED` error connecting to Valkey
**Solution:**
1. Ensure Valkey service is healthy: `docker-compose ps`
2. Check network connectivity: `docker network ls`
3. Verify `VALKEY_HOST` matches service name in docker-compose
### Session Expired Errors
**Problem:** `Session expired` errors during testing
**Solution:**
1. Increase `MCP_SESSION_TIMEOUT` in `.env`
2. Ensure consistent session ID usage
3. Check system time synchronization
### PNR Not Found
**Problem:** `Booking not found` when retrieving by PNR
**Solution:**
1. Verify PNR format: `TEST-XXXXXX`
2. Confirm session ID matches booking session
3. Check Valkey data persistence: `docker-compose logs valkey`
## Performance
- **Search Response Time**: < 100ms (mock data generation)
- **Booking Creation**: < 200ms (Valkey write)
- **Concurrent Sessions**: 50+ simultaneous MCP connections
- **Session Capacity**: 1000+ bookings per session
- **Memory Footprint**: ~50MB (Node.js + dependencies)
## Contributing
This is a mock server for testing purposes. Contributions welcome for:
- Additional airline/airport data
- Enhanced mock data realism
- Performance optimizations
- Bug fixes
## License
MIT License - See LICENSE file for details
## Support
For issues and questions:
- GitHub Issues: <repository-url>/issues
- Documentation: See `/specs` directory for detailed design docs
---
**Remember**: This is a MOCK server. All bookings are simulated. Never use in production systems requiring real GDS connectivity.

188
SAFETY_DISCLAIMER.md Normal file
View File

@@ -0,0 +1,188 @@
# SAFETY DISCLAIMER
## ⚠️ FOR TESTING AND DEMONSTRATION PURPOSES ONLY ⚠️
This Mock GDS MCP Server is designed **EXCLUSIVELY** for:
- ✅ Software testing and quality assurance
- ✅ Developer training and education
- ✅ Sales demonstrations and proof-of-concept
- ✅ Integration testing with MCP clients
- ✅ Automated test suite execution
---
## What This Server Does NOT Do
### ❌ NO Real GDS Connections
- This server does NOT connect to any real Global Distribution Systems
- Does NOT communicate with Amadeus, SABRE, Galileo, or any production GDS
- Does NOT access real flight inventory, hotel availability, or car rental systems
- All data is generated locally from mock data files
### ❌ NO Real Transactions
- Does NOT process real bookings
- Does NOT charge credit cards or payment instruments
- Does NOT create actual travel reservations
- Does NOT send confirmation emails to airlines or travel suppliers
- Does NOT issue real tickets or vouchers
### ❌ NO Real Passenger Data
- Does NOT store or transmit real passenger PII (Personally Identifiable Information)
- Does NOT connect to passenger databases
- Does NOT perform identity verification
- All passenger data is ephemeral and session-scoped
### ❌ NO Production Use
- This server is NOT certified or approved for production use
- Does NOT provide SLA guarantees or uptime commitments
- Does NOT handle real customer transactions
- Does NOT meet regulatory compliance requirements for real bookings
---
## What This Server DOES Do
### ✅ Mock Data Generation
- Generates realistic flight, hotel, and car rental options
- Uses valid IATA airport codes and airline identifiers
- Simulates pricing, availability, and schedule data
- Creates professional-looking mock responses
### ✅ Test-Prefixed Identifiers
- **ALL PNRs (Passenger Name Records) have `TEST-` prefix**
- Format: `TEST-XXXXXX` (e.g., `TEST-A1B2C3`)
- This prefix clearly identifies all bookings as test/mock data
- Production systems should REJECT any PNR starting with `TEST-`
### ✅ Session Isolation
- Each MCP session maintains isolated booking state
- Sessions auto-expire after timeout (default 1 hour)
- Data is stored in Valkey (Redis-compatible) with TTL
- No cross-session data leakage
### ✅ Full MCP Protocol Compliance
- Implements Model Context Protocol specification correctly
- Provides proper tool schemas and validation
- Returns standard error codes and responses
- Works with any MCP-compliant client
---
## Safety Guarantees
### 1. No External API Calls
**Guarantee**: This server makes ZERO external API calls to production systems.
**Verification**:
- Review `src/data/` directory - all data is embedded
- Check network logs - no outbound HTTP/HTTPS connections
- Inspect Docker network configuration - isolated network only
### 2. Test-Only PNR Format
**Guarantee**: All PNR codes follow `TEST-XXXXXX` format.
**Verification**:
- See `src/data/pnr.js` - `generatePNR()` function enforces prefix
- Validation in `src/utils/errors.js` - `validatePNR()` requires `TEST-` prefix
- Production systems should have safeguards to reject `TEST-` prefixed bookings
### 3. Local Data Storage Only
**Guarantee**: All booking data stays within the Valkey container.
**Verification**:
- Check `docker-compose.yaml` - Valkey not exposed publicly
- Review `src/session/storage.js` - only connects to local Valkey
- Data persists only during container lifetime (configurable)
### 4. Non-Root Container Execution
**Guarantee**: Docker container runs as non-root user for security.
**Verification**:
- See `docker/Dockerfile` - creates `gds` user (UID 1001)
- Container runs with `USER gds` directive
- Cannot write to host system outside mounted volumes
---
## Configuration Safety
### Safe Defaults
The server ships with safe default configuration:
```bash
MOCK_DATA_SEED=random # Non-deterministic mock data
LOG_LEVEL=info # Appropriate logging verbosity
MCP_SESSION_TIMEOUT=3600 # 1-hour session expiry
VALKEY_HOST=localhost # Local-only connection
```
### Unsafe Configurations (Don't Do This!)
**DO NOT** set `VALKEY_HOST` to a production database
**DO NOT** modify PNR generation to remove `TEST-` prefix
**DO NOT** connect this server to real GDS credentials
**DO NOT** use this server in customer-facing production systems
---
## Regulatory Compliance
### This Server Is NOT:
- PCI DSS compliant (does not handle real payment data)
- GDPR compliant (not designed for real personal data)
- IATA NDC certified (not a real distribution system)
- SOC 2 audited (no production security controls)
### Use Cases That Require Real GDS:
If you need any of the following, use a **real GDS system**, not this mock server:
- Real flight bookings for customers
- Actual hotel reservations
- Live car rental confirmations
- Ticketing and payment processing
- Customer itinerary management
- Regulatory reporting and auditing
---
## Liability Disclaimer
**NO WARRANTY**: This software is provided "as is" without warranty of any kind.
**NO LIABILITY**: The authors are not liable for:
- Loss of data
- Failed bookings or reservations
- Financial losses
- Service interruptions
- Any damages arising from use or misuse
**USER RESPONSIBILITY**: Users are responsible for:
- Ensuring appropriate use (testing only)
- Not using in production systems
- Complying with applicable laws and regulations
- Properly disclaiming mock data in demonstrations
---
## Acknowledgment
By using this Mock GDS MCP Server, you acknowledge that:
1. ✅ I understand this is a mock/test server only
2. ✅ I will NOT use this in production systems
3. ✅ I will NOT process real customer bookings
4. ✅ I will clearly label all demonstrations as "mock data"
5. ✅ I accept full responsibility for appropriate use
---
## Questions or Concerns?
If you have questions about safe and appropriate use of this mock server:
- Review the documentation in `/specs` directory
- Check the README.md for usage examples
- File an issue on the project repository
- Consult with your legal and compliance teams before use
---
**Last Updated**: 2026-04-07
**Version**: 0.1.0
**Status**: TEST/DEMO USE ONLY

62
docker-compose.yaml Normal file
View File

@@ -0,0 +1,62 @@
services:
valkey:
image: valkey/valkey:8.0-alpine
container_name: gds-mock-valkey
ports:
- "6379:6379"
volumes:
- valkey_data:/data
command: >
valkey-server
--save 60 1
--loglevel warning
--maxmemory 256mb
--maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
restart: unless-stopped
networks:
- gds-network
gds-mock-mcp:
build:
context: .
dockerfile: docker/Dockerfile
container_name: gds-mock-mcp
depends_on:
valkey:
condition: service_healthy
ports:
- "${PORT:-3000}:3000" # HTTP port for MCP Streamable HTTP transport
environment:
- NODE_ENV=production
- LOG_LEVEL=${LOG_LEVEL:-info}
- VALKEY_HOST=valkey
- VALKEY_PORT=6379
- MCP_SESSION_TIMEOUT=${MCP_SESSION_TIMEOUT:-3600}
- MOCK_DATA_SEED=${MOCK_DATA_SEED:-random}
- PORT=3000
- HOST=0.0.0.0 # Bind to all interfaces in Docker
restart: unless-stopped
networks:
- gds-network
stdin_open: true
tty: true
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
volumes:
valkey_data:
driver: local
networks:
gds-network:
driver: bridge

49
docker/Dockerfile Normal file
View File

@@ -0,0 +1,49 @@
# Stage 1: Builder
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Stage 2: Production
FROM node:20-alpine
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1001 -S gds && \
adduser -S -D -H -u 1001 -h /app -s /sbin/nologin -G gds -g gds gds
WORKDIR /app
# Copy dependencies from builder
COPY --from=builder --chown=gds:gds /app/node_modules ./node_modules
# Copy application source
COPY --chown=gds:gds src ./src
COPY --chown=gds:gds package.json ./
# Set environment variables
ENV NODE_ENV=production \
LOG_LEVEL=info \
VALKEY_HOST=valkey \
VALKEY_PORT=6379
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "const Redis = require('ioredis'); const client = new Redis({ host: process.env.VALKEY_HOST, port: process.env.VALKEY_PORT, lazyConnect: true }); client.connect().then(() => client.ping()).then(() => process.exit(0)).catch(() => process.exit(1));"
# Switch to non-root user
USER gds
# Expose MCP server (stdio, no network port)
# MCP servers use stdio transport, not network ports
# Start server with dumb-init
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["node", "src/index.js"]

21
docker/docker-bake.hcl Normal file
View File

@@ -0,0 +1,21 @@
target "default" {
context = ".."
dockerfile = "docker/Dockerfile"
platforms = ["linux/amd64", "linux/arm64"]
tags = [
"gds-mock-mcp:latest",
"gds-mock-mcp:0.1.0"
]
}
target "amd64" {
inherits = ["default"]
platforms = ["linux/amd64"]
tags = ["gds-mock-mcp:0.1.0-amd64"]
}
target "arm64" {
inherits = ["default"]
platforms = ["linux/arm64"]
tags = ["gds-mock-mcp:0.1.0-arm64"]
}

75
docker/nginx.conf.example Normal file
View File

@@ -0,0 +1,75 @@
# Nginx Configuration for Optional HTTP/2 Upgrade
# Client-facing: HTTP/2
# Backend: HTTP/1.1 (MCP server)
upstream gds_mcp_backend {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name gds-mock-mcp.example.com;
# SSL configuration (replace with your certificates)
ssl_certificate /path/to/ssl/certificate.crt;
ssl_certificate_key /path/to/ssl/private.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Logging
access_log /var/log/nginx/gds-mcp-access.log;
error_log /var/log/nginx/gds-mcp-error.log;
# MCP endpoint proxy
location /mcp {
proxy_pass http://gds_mcp_backend;
proxy_http_version 1.1;
# Preserve headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# MCP headers
proxy_set_header MCP-Protocol-Version $http_mcp_protocol_version;
proxy_set_header MCP-Session-Id $http_mcp_session_id;
proxy_set_header Last-Event-ID $http_last_event_id;
# SSE configuration
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Health check endpoint
location /health {
proxy_pass http://gds_mcp_backend;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
}
# Rate limiting (additional layer beyond application)
limit_req_zone $binary_remote_addr zone=mcp_limit:10m rate=100r/m;
limit_req zone=mcp_limit burst=20 nodelay;
# Security headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
}
# HTTP redirect to HTTPS
server {
listen 80;
server_name gds-mock-mcp.example.com;
return 301 https://$server_name$request_uri;
}

43
eslint.config.js Normal file
View File

@@ -0,0 +1,43 @@
export default [
{
ignores: [
'node_modules/',
'dist/',
'build/',
'coverage/',
'*.min.js',
'.git/',
'docker/'
]
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
globals: {
console: 'readonly',
process: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
setTimeout: 'readonly',
setInterval: 'readonly',
clearTimeout: 'readonly',
clearInterval: 'readonly'
}
},
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-console': 'off',
'prefer-const': 'error',
'no-var': 'error',
'eqeqeq': ['error', 'always'],
'curly': ['error', 'all'],
'brace-style': ['error', '1tbs'],
'comma-dangle': ['error', 'never'],
'quotes': ['error', 'single', { avoidEscape: true }],
'semi': ['error', 'always']
}
}
];

3105
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

54
package.json Normal file
View File

@@ -0,0 +1,54 @@
{
"name": "gds-mock-mcp",
"version": "0.1.0",
"description": "Mock Global Distribution System (GDS) MCP Server for testing and demonstration",
"type": "module",
"main": "dist/index.js",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts --verbose --log-level DEBUG",
"start:js": "node src/index.ts",
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
"lint": "eslint .",
"format": "prettier --write .",
"docker:build": "docker build -t gds-mock-mcp -f docker/Dockerfile .",
"docker:run": "docker-compose up",
"docker:down": "docker-compose down"
},
"dependencies": {
"@cfworker/json-schema": "^4.1.1",
"@modelcontextprotocol/express": "^2.0.0-alpha.2",
"@modelcontextprotocol/node": "^2.0.0-alpha.2",
"@modelcontextprotocol/sdk": "^1.29.0",
"@modelcontextprotocol/server": "^2.0.0-alpha.2",
"cors": "^2.8.5",
"express": "^5.2.1",
"express-rate-limit": "^8.3.2",
"ioredis": "^5.4.1",
"pino": "^9.5.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/node": "^25.6.0",
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"tsx": "^4.21.0",
"typescript": "^6.0.2"
},
"keywords": [
"mcp",
"gds",
"mock",
"travel",
"booking",
"testing"
],
"author": "",
"license": "MIT"
}

View File

@@ -0,0 +1,62 @@
# Specification Quality Checklist: Mock GDS MCP Server
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2025-01-22
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Validation Summary
**Status**: ✅ PASSED
All checklist items have been validated:
### Content Quality
- ✅ Specification is technology-agnostic - no mention of specific programming languages, frameworks, databases, or implementation technologies
- ✅ All content focuses on WHAT users need (testing/demos) and WHY (development efficiency, reliable workflows)
- ✅ Written in plain language suitable for stakeholders - uses business terms (developers, QA teams, sales demos) rather than technical jargon
- ✅ All mandatory sections (User Scenarios, Requirements, Success Criteria, Assumptions) are complete and detailed
### Requirement Completeness
- ✅ Zero [NEEDS CLARIFICATION] markers - all requirements are fully specified with concrete details
- ✅ Each functional requirement is testable - FR-002 can be tested by executing searches with specified parameters and verifying outputs; FR-009 can be tested by running concurrent sessions and checking for data leakage
- ✅ All success criteria include specific metrics (SC-001: 30 seconds, SC-002: 50 concurrent sessions, SC-003: under 2 seconds, SC-004: 100% valid codes)
- ✅ Success criteria are technology-agnostic - no implementation details (e.g., SC-001 measures workflow completion time, not "API response time")
- ✅ 5 user stories with 15 total acceptance scenarios covering all major workflows (flight booking, multi-service, car rental, session isolation, demo data)
- ✅ 8 edge cases identified covering error scenarios (invalid codes, cancelled bookings, session expiry, validation failures)
- ✅ Scope clearly bounded in Assumptions section - defines what's included (major airports, common workflows) and excluded (comprehensive airport coverage, complex fare rules, production use)
- ✅ 12 assumptions documented covering data scope, pricing, payment handling, session management, and target users
### Feature Readiness
- ✅ Each FR has corresponding acceptance scenarios - FR-002 (flight search) maps to User Story 1 acceptance scenario 1; FR-009 (session isolation) maps to User Story 4 scenarios
- ✅ User scenarios cover all primary flows: basic flight booking (P1), multi-service bundling (P2), complete packages (P3), concurrent testing (P2), demo scenarios (P3)
- ✅ Success criteria align with feature goals: rapid workflow completion (SC-001), concurrent session support (SC-002), realistic data quality (SC-004, SC-006), testing effectiveness (SC-005, SC-008)
- ✅ No implementation leakage detected - specification never mentions MCP implementation details, data storage mechanisms, or specific tool implementations
## Notes
Specification is ready for next phase. Can proceed directly to `/speckit.plan` for implementation planning, or optionally run `/speckit.clarify` if additional stakeholder input is desired (though no clarifications are currently needed).

View File

@@ -0,0 +1,746 @@
# MCP Tool Contracts: Mock GDS MCP Server
**Branch**: `001-mock-gds-server` | **Date**: 2026-04-07
## Overview
This document defines the MCP tool schemas exposed by the Mock GDS MCP server. All tools follow the MCP JSON-RPC 2.0 protocol and include comprehensive JSON Schema validation.
## Tool Categories
1. **Flight Operations**: searchFlights, bookFlight
2. **Hotel Operations**: searchHotels, bookHotel
3. **Car Rental Operations**: searchCars, bookCar
4. **Booking Management**: retrieveBooking, cancelBooking, listBookings
---
## Flight Operations
### searchFlights
Search for available flight options.
**Tool Name**: `searchFlights`
**Description**: Search for flights between two airports on a specific date. Returns mock flight options with realistic pricing, schedules, and availability.
**Input Schema**:
```json
{
"type": "object",
"properties": {
"origin": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"description": "Origin airport IATA code (3 letters, e.g., 'JFK')"
},
"destination": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"description": "Destination airport IATA code (3 letters, e.g., 'LAX')"
},
"departureDate": {
"type": "string",
"format": "date",
"description": "Departure date in ISO 8601 format (YYYY-MM-DD)"
},
"passengers": {
"type": "object",
"properties": {
"adults": {
"type": "integer",
"minimum": 1,
"maximum": 9,
"default": 1
},
"children": {
"type": "integer",
"minimum": 0,
"maximum": 9,
"default": 0
},
"infants": {
"type": "integer",
"minimum": 0,
"maximum": 9,
"default": 0
}
},
"required": ["adults"]
},
"cabin": {
"type": "string",
"enum": ["economy", "premium_economy", "business", "first"],
"default": "economy"
}
},
"required": ["origin", "destination", "departureDate"]
}
```
**Example Request**:
```json
{
"origin": "JFK",
"destination": "LAX",
"departureDate": "2026-06-15",
"passengers": { "adults": 2 },
"cabin": "economy"
}
```
---
### bookFlight
Create a flight booking.
**Tool Name**: `bookFlight`
**Description**: Book one or more flight segments with passenger details. Creates a PNR (Passenger Name Record) with TEST- prefix.
**Input Schema**:
```json
{
"type": "object",
"properties": {
"flightIds": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"description": "Array of flight IDs from search results"
},
"passengers": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["adult", "child", "infant"]
},
"firstName": { "type": "string", "minLength": 1, "maxLength": 50 },
"lastName": { "type": "string", "minLength": 1, "maxLength": 50 },
"dateOfBirth": { "type": "string", "format": "date" },
"email": { "type": "string", "format": "email" },
"phone": { "type": "string" },
"frequentFlyerNumber": { "type": "string" }
},
"required": ["type", "firstName", "lastName"]
}
},
"contactEmail": {
"type": "string",
"format": "email",
"description": "Primary contact email"
},
"contactPhone": {
"type": "string",
"description": "Primary contact phone"
}
},
"required": ["flightIds", "passengers"],
"anyOf": [
{ "required": ["contactEmail"] },
{ "required": ["contactPhone"] }
]
}
```
---
## Hotel Operations
### searchHotels
**Tool Name**: `searchHotels`
**Description**: Search for hotel properties in a city with check-in/check-out dates.
**Input Schema**:
```json
{
"type": "object",
"properties": {
"cityCode": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"description": "City IATA code (3 letters, e.g., 'LAX')"
},
"checkInDate": {
"type": "string",
"format": "date"
},
"checkOutDate": {
"type": "string",
"format": "date"
},
"guests": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"default": 1
},
"starRating": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"description": "Minimum star rating filter (optional)"
}
},
"required": ["cityCode", "checkInDate", "checkOutDate"]
}
```
---
### bookHotel
**Tool Name**: `bookHotel`
**Description**: Book a hotel reservation. Can create a new PNR or add to an existing flight booking.
**Input Schema**:
```json
{
"type": "object",
"properties": {
"hotelId": { "type": "string" },
"existingPnr": {
"type": "string",
"pattern": "^TEST-[A-Z0-9]{6}$",
"description": "Optional: Add hotel to existing booking"
},
"guests": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"firstName": { "type": "string" },
"lastName": { "type": "string" },
"email": { "type": "string", "format": "email" }
},
"required": ["firstName", "lastName"]
}
},
"specialRequests": { "type": "string" }
},
"required": ["hotelId", "guests"]
}
```
---
## Car Rental Operations
### searchCars
**Tool Name**: `searchCars`
**Description**: Search for car rental options at an airport or city location.
**Input Schema**:
```json
{
"type": "object",
"properties": {
"pickupLocationCode": {
"type": "string",
"pattern": "^[A-Z]{3}$"
},
"dropoffLocationCode": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"description": "Defaults to pickup location if not specified"
},
"pickupDate": {
"type": "string",
"format": "date-time"
},
"dropoffDate": {
"type": "string",
"format": "date-time"
},
"driverAge": {
"type": "integer",
"minimum": 21,
"maximum": 99,
"default": 30
}
},
"required": ["pickupLocationCode", "pickupDate", "dropoffDate"]
}
```
---
### bookCar
**Tool Name**: `bookCar`
**Description**: Book a car rental. Can create a new PNR or add to an existing booking.
---
## Booking Management Operations
### retrieveBooking
**Tool Name**: `retrieveBooking`
**Description**: Fetch complete booking details including all segments.
**Input Schema**:
```json
{
"type": "object",
"properties": {
"pnr": {
"type": "string",
"pattern": "^TEST-[A-Z0-9]{6}$"
}
},
"required": ["pnr"]
}
```
---
### cancelBooking
**Tool Name**: `cancelBooking`
**Description**: Cancel a confirmed booking.
**Input Schema**:
```json
{
"type": "object",
"properties": {
"pnr": {
"type": "string",
"pattern": "^TEST-[A-Z0-9]{6}$"
},
"reason": { "type": "string" }
},
"required": ["pnr"]
}
```
---
### listBookings
**Tool Name**: `listBookings`
**Description**: Retrieve a list of all PNRs created in the current MCP session.
**Input Schema**:
```json
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["all", "confirmed", "cancelled"],
"default": "all"
}
}
}
```
---
## Error Responses
All tools return standard MCP errors:
- **-32602**: Invalid params (validation failure)
- **-32603**: Internal error (server error)
- **-32001**: Resource not found (invalid PNR, airport code, etc.)
- **-32002**: Business logic error (cancelled booking, date in past, etc.)
**Error Format**:
```json
{
"code": -32602,
"message": "Invalid airport code 'XYZ': must be a valid 3-letter IATA code",
"data": {
"field": "origin",
"value": "XYZ"
}
}
```
---
## MCP Resource Endpoints
- `gds://session/current` - Current session metadata
- `gds://session/bookings` - List of all bookings
- `gds://mock-data/airports` - Mock airport data
- `gds://mock-data/airlines` - Mock airline data
---
## Protocol Notes
- All tools use JSON-RPC 2.0 over stdio (default) or SSE
- Tool schemas validated before execution
- Timestamps use ISO 8601 format
- Prices in USD cents (integer)
- All PNRs prefixed with "TEST-" for safety
---
## Remote Access Endpoints (Added 2026-04-07)
The following endpoints are available when running in HTTP/2 remote transport mode.
### Health Check Endpoint
**Endpoint**: `GET /health`
**Description**: Returns server operational status for monitoring and health checks. This endpoint is unauthenticated and exempt from rate limiting.
**Use Cases**:
- Load balancer health checks
- Monitoring system integration (Prometheus, Datadog, etc.)
- Deployment validation
- Debugging connection and memory issues
**HTTP Method**: GET
**Authentication**: None (publicly accessible)
**Rate Limiting**: Exempt (not subject to rate limits)
**Request**: No body required
**Response Schema**:
```json
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["healthy", "degraded", "unhealthy"],
"description": "Overall server health status"
},
"uptime": {
"type": "integer",
"description": "Server uptime in seconds since start"
},
"version": {
"type": "string",
"description": "Server version (from package.json)"
},
"connections": {
"type": "object",
"properties": {
"stdio": {
"type": "integer",
"description": "Active stdio connections (0 or 1)"
},
"http": {
"type": "integer",
"description": "Active HTTP/2 connections"
},
"total": {
"type": "integer",
"description": "Total active connections"
}
},
"required": ["stdio", "http", "total"]
},
"sessions": {
"type": "object",
"properties": {
"active": {
"type": "integer",
"description": "Sessions with recent activity (within 5 minutes)"
},
"total": {
"type": "integer",
"description": "Total sessions in storage"
}
},
"required": ["active", "total"]
},
"storage": {
"type": "object",
"properties": {
"connected": {
"type": "boolean",
"description": "Valkey connection status"
},
"responseTime": {
"type": "number",
"description": "Valkey ping response time in milliseconds"
}
},
"required": ["connected", "responseTime"]
},
"memory": {
"type": "object",
"properties": {
"used": {
"type": "number",
"description": "Process memory used in MB"
},
"total": {
"type": "number",
"description": "Total system memory in MB"
},
"percentage": {
"type": "number",
"description": "Memory usage percentage (0-1)"
}
},
"required": ["used", "total", "percentage"]
},
"timestamp": {
"type": "integer",
"description": "Health check timestamp (Unix milliseconds)"
}
},
"required": ["status", "uptime", "version", "connections", "sessions", "storage", "memory", "timestamp"]
}
```
**Status Determination**:
- **healthy** (200 OK): All systems operational
- Storage connected
- Memory usage < 80%
- Valkey response time < 100ms
- **degraded** (200 OK): Some issues but still serving requests
- Storage slow (ping 100-500ms)
- Memory usage 80-90%
- **unhealthy** (503 Service Unavailable): Critical issues
- Storage disconnected
- Memory usage > 90%
- Unable to serve requests
**Example Response (Healthy)**:
```json
{
"status": "healthy",
"uptime": 3600,
"version": "0.1.0",
"connections": {
"stdio": 0,
"http": 12,
"total": 12
},
"sessions": {
"active": 8,
"total": 15
},
"storage": {
"connected": true,
"responseTime": 2.5
},
"memory": {
"used": 45.2,
"total": 8192,
"percentage": 0.0055
},
"timestamp": 1712486460000
}
```
**Example Response (Degraded)**:
```json
{
"status": "degraded",
"uptime": 7200,
"version": "0.1.0",
"connections": {
"stdio": 0,
"http": 45,
"total": 45
},
"sessions": {
"active": 42,
"total": 50
},
"storage": {
"connected": true,
"responseTime": 150
},
"memory": {
"used": 6553.6,
"total": 8192,
"percentage": 0.8
},
"timestamp": 1712490060000
}
```
**Example Response (Unhealthy)**:
```json
{
"status": "unhealthy",
"uptime": 10800,
"version": "0.1.0",
"connections": {
"stdio": 0,
"http": 0,
"total": 0
},
"sessions": {
"active": 0,
"total": 0
},
"storage": {
"connected": false,
"responseTime": null
},
"memory": {
"used": 120,
"total": 8192,
"percentage": 0.015
},
"timestamp": 1712493660000
}
```
**cURL Example**:
```bash
curl -i http://localhost:8080/health
```
**Load Balancer Integration** (Nginx):
```nginx
upstream mcp_backend {
server mcp-server:3000;
health_check interval=10s uri=/health;
}
```
**Prometheus Monitoring**:
```yaml
scrape_configs:
- job_name: 'mcp-server'
static_configs:
- targets: ['localhost:8080']
metrics_path: '/health'
scrape_interval: 30s
```
---
## Rate Limiting (HTTP Mode Only)
When running in HTTP/2 remote mode, all MCP tool endpoints are subject to rate limiting.
**Algorithm**: Sliding Window Counter (Hybrid)
**Default Limits**:
- 100 requests per minute per client IP address
- Window size: 60 seconds
- Burst tolerance: ~150 requests in worst-case window overlap
**Rate Limit Headers** (included in all HTTP responses):
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1712486520
```
**Rate Limit Exceeded Response** (429 Too Many Requests):
```json
{
"error": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED",
"limit": 100,
"current": 105,
"resetAt": "2026-04-07T10:02:00.000Z",
"retryAfter": 15
}
```
**HTTP Headers on 429**:
```
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1712486520
Retry-After: 15
Content-Type: application/json
```
**Exemptions**:
- Health check endpoint (`/health`) is exempt from rate limiting
- CORS preflight requests (OPTIONS) are exempt
**Configuration**:
```bash
RATE_LIMIT_ENABLED=true
RATE_LIMIT_PER_MINUTE=100
RATE_LIMIT_WINDOW_SECONDS=60
```
---
## CORS Headers (HTTP Mode Only)
When running in HTTP/2 remote mode, all endpoints support cross-origin requests.
**CORS Policy**: Permissive wildcard (`Access-Control-Allow-Origin: *`)
**Preflight Request** (OPTIONS):
```
OPTIONS /mcp HTTP/1.1
Origin: https://web-client.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, MCP-Session-ID
```
**Preflight Response**:
```
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, MCP-Session-ID, X-Requested-With
Access-Control-Max-Age: 86400
```
**Actual Request Headers** (added to all responses):
```
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: false
Access-Control-Expose-Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
```
**Security Note**: Wildcard CORS is appropriate for development/testing with mock data only. Deploy within private networks (VPN, firewalls) for security.
---
## Transport Mode Configuration
**Environment Variables**:
```bash
# Transport selection
TRANSPORT_MODE=stdio|http|both # Default: stdio
# HTTP/2 configuration (when http or both)
HTTP_PORT=3000 # Internal port (default: 3000)
HTTP_HOST=127.0.0.1 # Bind address (default: 127.0.0.1 for proxy)
# External access via reverse proxy
# Nginx listens on :8080 (HTTP/2 + TLS)
# Proxies to mcp-server:3000 (HTTP/1.1)
```
**Protocol Notes (Updated)**:
- stdio mode: JSON-RPC 2.0 over stdin/stdout (local only)
- http mode: JSON-RPC 2.0 over SSE via StreamableHTTPServerTransport
- HTTP/2: Provided by nginx reverse proxy (terminates HTTP/2, proxies HTTP/1.1)
- CORS: Enabled in http mode, wildcard policy
- Rate limiting: Enabled in http mode, IP-based tracking
- Health checks: Available in http mode at `/health`

View File

@@ -0,0 +1,708 @@
# 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
---
## Remote Access Entities (Added 2026-04-07)
The following entities support remote MCP access over HTTP/2 with rate limiting, health monitoring, and global PNR retrieval.
### 8. Remote Connection
Represents an active remote client connection over HTTP/2.
**Fields**:
```typescript
{
connectionId: string; // Unique connection identifier (UUID)
sessionId: string; // Associated MCP session ID
remoteIP: string; // Client IP address (for rate limiting)
connectedAt: number; // Unix timestamp (ms)
lastActivityAt: number; // Unix timestamp (ms)
transportType: string; // 'http' | 'stdio'
userAgent: string; // Client user agent
requestCount: number; // Total requests made in this connection
}
```
**Storage**: Valkey hash at key `connection:{connectionId}`
**Validation**:
- `remoteIP` must be valid IPv4 or IPv6 format
- `transportType` must be 'http' or 'stdio'
- `connectedAt` must be <= `lastActivityAt`
**State Transitions**:
```
[Connected] → [Active] → [Idle] → [Disconnected]
[Expired]
```
**Lifecycle**:
- Created on initial HTTP request with new session
- Updated on every request (lastActivityAt, requestCount)
- Auto-expires after SESSION_TTL_HOURS of inactivity
- Explicitly deleted on graceful disconnect
---
### 9. Rate Limit Record
Tracks request rates per client IP address for abuse prevention.
**Fields**:
```typescript
{
clientIP: string; // Client IP address (key component)
currentWindow: number; // Current time window (Unix timestamp / window_seconds)
currentCount: number; // Request count in current window
previousCount: number; // Request count in previous window
limit: number; // Max requests allowed per window
resetAt: number; // Next window reset timestamp
}
```
**Storage**:
- Current window: Valkey integer at key `ratelimit:{ip}:{currentWindow}`
- Previous window: Valkey integer at key `ratelimit:{ip}:{previousWindow}`
**Validation**:
- `clientIP` must be valid IP address
- `currentCount`, `previousCount` must be non-negative integers
- `limit` must be positive integer (default: 100)
- `resetAt` must be in the future
**Algorithm**: Sliding Window Counter (Hybrid)
```javascript
estimatedCount = (previousCount * previousWeight) + currentCount
where previousWeight = 1 - (elapsedInWindow / windowSeconds)
```
**Lifecycle**:
- Counter incremented on each request: `INCR ratelimit:{ip}:{window}`
- TTL set to 2× window duration (keeps previous window accessible)
- Auto-expires after TTL (no manual cleanup needed)
- Resets at window boundary (new window key)
**Error Response** (when limit exceeded):
```typescript
{
error: "Rate limit exceeded",
code: "RATE_LIMIT_EXCEEDED",
limit: 100,
current: 105,
resetAt: "2026-04-07T10:01:00.000Z",
retryAfter: 15 // seconds
}
```
---
### 10. Health Status
Represents server operational status for monitoring and health checks.
**Fields**:
```typescript
{
status: string; // 'healthy' | 'degraded' | 'unhealthy'
uptime: number; // Seconds since server start
version: string; // Server version
connections: {
stdio: number; // Active stdio connections (0 or 1 typically)
http: number; // Active HTTP connections
total: number; // Total active connections
},
sessions: {
active: number; // Active sessions (sessions with recent activity)
total: number; // Total sessions in Valkey
},
storage: {
connected: boolean; // Valkey connection status
responseTime: number; // Valkey ping response time (ms)
},
memory: {
used: number; // Process memory used (MB)
total: number; // Total system memory (MB)
percentage: number; // Memory usage percentage
},
timestamp: number; // Health check timestamp (Unix ms)
}
```
**Endpoint**: `GET /health` (unauthenticated, exempt from rate limiting)
**Status Determination**:
- **healthy**: All systems operational, storage connected, memory < 80%
- **degraded**: Storage slow (ping > 100ms) or memory 80-90%
- **unhealthy**: Storage disconnected or memory > 90%
**Response Codes**:
- 200 OK: status = 'healthy'
- 200 OK: status = 'degraded' (still serving requests)
- 503 Service Unavailable: status = 'unhealthy'
**Example Response**:
```json
{
"status": "healthy",
"uptime": 3600,
"version": "0.1.0",
"connections": {
"stdio": 0,
"http": 12,
"total": 12
},
"sessions": {
"active": 8,
"total": 15
},
"storage": {
"connected": true,
"responseTime": 2
},
"memory": {
"used": 45,
"total": 8192,
"percentage": 0.55
},
"timestamp": 1712486460000
}
```
**Use Cases**:
- Load balancer health checks
- Monitoring system integration (Prometheus, Datadog)
- Deployment validation (verify server started successfully)
- Debugging (check connection counts, memory usage)
---
## Updated Storage Schema (Remote Mode)
### Global PNR Storage (Session-Independent)
**Key Change**: PNRs now stored globally with TTL, not scoped to sessions.
```
pnr:{pnr} # Global PNR storage
→ {
pnr: string,
status: 'confirmed' | 'cancelled',
createdAt: ISO8601,
expiresAt: ISO8601,
creatingSessionId: string, # For logging only, not access control
segments: [...],
passengers: [...],
totalPrice: number
}
```
**TTL**: Configurable via `PNR_TTL_HOURS` (default 1 hour)
**Access**: Any session can retrieve any PNR (global retrieval)
**Expiration**: PNR auto-deleted by Valkey after TTL expires
---
### Session PNR Reference
Sessions track which PNRs they created (for `listBookings` tool):
```
session:{sessionId}:pnrs # Set of PNR codes created in this session
→ Set<string> # e.g., ["TEST-ABC123", "TEST-DEF456"]
```
**Purpose**: Enable `listBookings` to return session-created PNRs
**Lifecycle**: Deleted when session expires (PNRs persist independently)
---
### Rate Limit Keys
```
ratelimit:{ip}:{window} # Request count for IP in time window
→ integer # e.g., 45 (requests made)
TTL: windowSeconds * 2 # Keep previous window accessible
```
**Example**:
```
ratelimit:192.168.1.1:287456 # Current window (e.g., minute 287456)
→ 45
ratelimit:192.168.1.1:287455 # Previous window
→ 92
```
---
### Connection Tracking
```
connection:{connectionId} # Remote connection metadata
→ { connectionId, sessionId, remoteIP, connectedAt, ... }
TTL: SESSION_TTL_HOURS
```
**Purpose**: Track active HTTP connections for health monitoring
**Cleanup**: Auto-expires with session TTL
---
## Updated Validation Rules (Remote Mode)
### Additional Validations
1. **IP Address Validation**:
- Must be valid IPv4 (e.g., `192.168.1.1`) or IPv6 format
- Used for rate limiting and logging
- Extracted from `X-Forwarded-For` or `X-Real-IP` headers (trusted proxy)
2. **Session ID Format** (HTTP mode):
- Must be valid UUID v4
- Sent via `MCP-Session-ID` header
- Generated by transport if not provided
3. **Rate Limit Headers** (HTTP mode):
- `X-RateLimit-Limit`: Integer > 0
- `X-RateLimit-Remaining`: Integer >= 0
- `X-RateLimit-Reset`: Unix timestamp
- `Retry-After`: Seconds (when limit exceeded)
4. **CORS Headers** (HTTP mode):
- `Origin`: Any (wildcard policy)
- `Access-Control-Allow-Origin`: Must be `*`
- Preflight requests must use OPTIONS method
---
## Performance Considerations (Remote Mode)
### Additional Overhead
- **Rate Limiting**: +3 Valkey ops per request (~1-2ms overhead)
- **Connection Tracking**: +1 Valkey write per request (~0.5ms overhead)
- **CORS Preflight**: OPTIONS requests handled immediately (no tool execution)
- **Health Checks**: Separate fast path (no session/rate limit checks)
### Expected Performance (Remote)
- **Search Operations**: <2s (requirement: SC-003)
- **Booking Operations**: <500ms (includes rate limit check)
- **Retrieval Operations**: <200ms (global PNR lookup)
- **Health Check**: <100ms (Valkey ping only)
- **Concurrent Remote Sessions**: 50+ (requirement: SC-012)
### Optimization Strategies
1. **Rate Limit Caching**: Cache IP counters in-memory for 1 second (reduce Valkey ops)
2. **Connection Pooling**: Reuse HTTP/2 connections (handled by Nginx)
3. **Health Check Caching**: Cache health status for 5 seconds
4. **CORS Preflight Caching**: 24-hour `Access-Control-Max-Age`
---
## Next Steps
Data model complete with remote access entities. Proceed to:
1. ✅ Update contracts/ with health endpoint schema
2. ✅ Update quickstart.md with remote access setup
3. ✅ Run agent context update script

View File

@@ -0,0 +1,258 @@
# Implementation Plan: Mock GDS MCP Server
**Branch**: `001-mock-gds-server` | **Date**: 2026-04-11 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/001-mock-gds-server/spec.md`
## Summary
Build a Mock Global Distribution System (GDS) server exposing flight, hotel, and car rental booking operations as Model Context Protocol (MCP) tools via HTTP transport. The server provides realistic mock data for testing travel applications without connecting to real GDS systems, with session-based state management and multi-service booking capabilities.
## Technical Context
**Language/Version**: Node.js 20 LTS with TypeScript 6.0.2
**Primary Dependencies**: @modelcontextprotocol/sdk ^1.0.4 (MCP protocol), @modelcontextprotocol/server ^2.0.0-alpha.2 (McpServer), @modelcontextprotocol/express ^2.0.0-alpha.2 (HTTP transport), ioredis ^5.4.1 (Valkey client), Express ^5.2.1, pino ^9.5.0 (structured logging)
**Storage**: Valkey 8.0+ (Redis-compatible in-memory store with persistence for PNRs and sessions)
**Testing**: Node.js built-in test runner or Vitest (TBD in Phase 0)
**Target Platform**: Linux/macOS servers, Docker containers (multi-platform: linux/amd64, linux/arm64)
**Project Type**: MCP server (HTTP-based tool provider for AI agents and MCP clients)
**Performance Goals**: Support 100+ concurrent sessions, <200ms p95 response time for search operations, <500ms for booking operations
**Constraints**: Stateless HTTP transport design (no server-side session affinity required), connection pooling for Valkey, structured logging for observability, all operations deterministic for given inputs
**Scale/Scope**: 11 MCP tools (search/book/cancel for flights/hotels/cars, plus session management), ~5000 lines of TypeScript, realistic mock data (100+ airports, 20+ airlines, 50+ hotels, 15+ car companies)
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
### I. MCP Protocol Compliance ✅
- **Status**: COMPLIANT
- **Evidence**: Using official @modelcontextprotocol/server package with McpServer class, all tools registered via standard API, StreamableHTTPServerTransport for MCP HTTP specification compliance
- **Validation**: McpServer handles JSON-RPC 2.0 message format automatically, capabilities declared in server initialization, error responses use MCP error structures
### II. Mock Data Realism ✅
- **Status**: COMPLIANT
- **Evidence**: Mock data uses real IATA airport codes (JFK, LAX, etc.), valid airline codes (AA, DL, UA), realistic pricing ($200-$800 flights, $80-$400 hotels), flight times respect geography and time zones
- **Implementation**: data/ directory contains realistic airlines, airports, flights, hotels, cars with internally consistent relationships
### III. No Real Transactions (Safety Guarantee) ✅
- **Status**: COMPLIANT
- **Evidence**: Server is pure mock implementation with no external GDS connections, PNRs generated locally with clear test format, no production API keys accepted
- **Safety Markers**: Documentation includes "MOCK" and "FOR TESTING ONLY" disclaimers, no real booking system integration
### IV. Tool-Based Architecture ✅
- **Status**: COMPLIANT
- **Evidence**: 11 MCP tools exposed (searchFlights, bookFlight, searchHotels, bookHotel, searchCars, bookCar, retrieveBooking, cancelBooking, listBookings, getSessionInfo, clearSession)
- **Implementation**: Each tool registered with description and inputSchema, tools are independently callable, responses deterministic for same inputs
### V. Stateful Session Management ✅
- **Status**: COMPLIANT
- **Evidence**: Session manager tracks state per session ID, Valkey storage isolates bookings by session, PNRs stored with configurable TTL (default 1 hour)
- **Implementation**: SessionManager class provides create/get/update/delete operations, state transitions explicit (search → book → confirm)
### VI. Observable and Debuggable ✅
- **Status**: COMPLIANT
- **Evidence**: Structured logging via pino with configurable levels (INFO, DEBUG, ERROR), all tool calls logged with parameters and results, error messages include actionable guidance
- **Implementation**: Logger middleware logs all MCP requests, tool execution tracked with duration metrics, health endpoint for server status
**Gate Result**: ✅ PASS - All constitutional requirements met
## Project Structure
### Documentation (this feature)
```text
specs/001-mock-gds-server/
├── plan.md # This file (updated with TypeScript details)
├── research.md # Technology decisions and best practices
├── data-model.md # Entity definitions and state models
├── quickstart.md # Getting started guide
├── contracts/ # MCP tool schemas and API contracts
│ └── mcp-tools.md # Tool definitions with input/output schemas
├── spec.md # Feature specification
└── tasks.md # Implementation task breakdown
```
### Source Code (repository root)
```text
src/ # TypeScript source files
├── index.ts # Main entry point, tool registration
├── server.ts # GDSMockServer class (McpServer wrapper)
├── data/ # Mock data generators
│ ├── airlines.ts # Airline database and lookups
│ ├── airports.ts # Airport codes and geography
│ ├── flights.ts # Flight generation logic
│ ├── hotels.ts # Hotel data and pricing
│ ├── cars.ts # Car rental options
│ └── pnr.ts # PNR generation utilities
├── session/ # Session and storage management
│ ├── manager.ts # SessionManager class
│ ├── storage.ts # ValkeyStorage (Redis client wrapper)
│ └── valkey-event-store.ts # Event store for MCP resumability
├── tools/ # MCP tool implementations
│ ├── flights.ts # searchFlights, bookFlight
│ ├── hotels.ts # searchHotels, bookHotel
│ ├── cars.ts # searchCars, bookCar
│ ├── bookings.ts # retrieveBooking, cancelBooking, listBookings
│ └── session.ts # getSessionInfo, clearSession
├── transports/ # HTTP transport layer
│ ├── http-server.ts # StreamableHTTPServerTransport setup
│ └── factory.ts # Transport factory
├── middleware/ # Express middleware
│ ├── cors.ts # CORS policy (allow all origins)
│ ├── rate-limit.ts # IP-based rate limiting
│ ├── logger.ts # Request logging
│ ├── protocol-version.ts # MCP-Protocol-Version validation
│ └── message-normalization.ts # Request normalization
├── utils/ # Shared utilities
│ ├── errors.ts # Error classes (MCPError, ValidationError, etc.)
│ └── logger.ts # Pino logger configuration
└── validation/ # Input validation
└── validators.ts # Validation helpers
dist/ # Compiled JavaScript output (build artifacts)
├── index.js
├── server.js
└── [mirrors src/ structure]
tests/ # Test files (TBD)
├── unit/
├── integration/
└── contract/
docker/ # Docker configuration
├── Dockerfile # Multi-platform build
└── entrypoint.sh # Container startup script
tsconfig.json # TypeScript compiler configuration
package.json # Dependencies and scripts
```
**Structure Decision**: Single Node.js/TypeScript project with clear separation of concerns. Source in `src/` compiled to `dist/` via TypeScript compiler. MCP tools in `tools/`, data generators in `data/`, session management in `session/`, HTTP transport in `transports/`. Middleware chain handles CORS, rate limiting, protocol validation. Valkey for persistent storage. Docker support for containerized deployment.
## Complexity Tracking
**No constitution violations** - All core principles satisfied without requiring exceptions or workarounds.
## Phase 0: Research & Technology Decisions
### Technology Stack Rationale
#### Node.js 20 LTS + TypeScript
**Decision**: Use Node.js 20 LTS with TypeScript 6.0.2 for type safety and modern JavaScript features
**Rationale**:
- MCP SDK officially supports Node.js with comprehensive TypeScript typings
- Strong ecosystem for HTTP servers and real-time streaming (SSE)
- TypeScript provides compile-time type checking reducing runtime errors
- ES2022 features (top-level await, private fields) improve code quality
- Active LTS support ensures stability and security updates
**Alternatives Considered**:
- Python with FastAPI: Good MCP support but weaker typing story, slower cold starts
- Go: Excellent performance but less mature MCP ecosystem
- Deno: Modern runtime but MCP SDK compatibility uncertain
#### MCP SDK Architecture
**Decision**: Use high-level `McpServer` from `@modelcontextprotocol/server` package
**Rationale**:
- Simplified tool registration API: `registerTool(name, config, callback)`
- Automatic JSON-RPC 2.0 message handling
- Built-in error handling and protocol validation
- Recommended approach per MCP SDK examples
- Reduces boilerplate compared to low-level `Server` class
**Implementation**:
```typescript
const server = new McpServer(
{ name: 'gds-mock-mcp', version: '0.1.0' },
{ capabilities: { tools: {}, logging: {} } }
);
server.registerTool('searchFlights', { description, inputSchema }, handler);
```
#### Valkey for State Storage
**Decision**: Valkey 8.0+ (Redis-compatible) for session and booking storage
**Rationale**:
- In-memory performance (<1ms read/write) meets latency requirements
- TTL support for automatic PNR expiration (1 hour default)
- Connection pooling via ioredis client
- Redis protocol compatibility enables ecosystem tooling
- Optional persistence (RDB/AOF) for development convenience
**Alternatives Considered**:
- In-memory only: Fast but state lost on restart, not suitable for demos
- PostgreSQL: Overkill for mock data, adds deployment complexity
- SQLite: File-based but lacks TTL and concurrent access patterns
#### HTTP Transport Strategy
**Decision**: StreamableHTTPServerTransport in stateless mode with Express 5
**Rationale**:
- MCP HTTP specification (2025-11-25) mandates HTTP/1.1 + SSE
- Stateless mode allows horizontal scaling (no session affinity)
- Express 5 provides modern middleware architecture
- @modelcontextprotocol/express package simplifies integration
- Health endpoint (`/health`) for monitoring without MCP overhead
**Key Pattern**:
```typescript
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined // Stateless - clients manage sessions
});
await transport.handleRequest(req, res); // Processes MCP messages
```
#### Logging Strategy
**Decision**: Structured JSON logging via pino with configurable levels
**Rationale**:
- Structured logs enable automated analysis and monitoring
- Pino is fastest Node.js logger (benchmarks show 10x faster than Winston)
- Log levels (DEBUG, INFO, ERROR) support development and production
- Tool execution tracking with duration metrics
- Correlation IDs via session tracking
**Output Example**:
```json
{
"level": "INFO",
"time": "2026-04-11T18:37:43.129Z",
"service": "gds-mock-mcp",
"tool": "searchFlights",
"sessionId": "abc123",
"duration": 45,
"msg": "Tool execution completed"
}
```
### Testing Approach
**Decision**: Node.js built-in test runner with contract tests for MCP compliance
**Rationale**:
- Node.js 20 includes native test runner (no external dependencies)
- Contract tests validate MCP tool schemas and responses
- Integration tests cover multi-step workflows (search → book → cancel)
- Unit tests for data generators and validation logic
**Test Categories**:
1. **Contract Tests**: Verify MCP tool schemas match declared types
2. **Integration Tests**: End-to-end workflows with Valkey
3. **Unit Tests**: Data generation, validation, PNR logic
### Development Tooling
**Decision**:
- `tsx` for development mode with hot reload
- `tsc` for production builds
- ESLint + Prettier for code quality
- Docker multi-platform builds (linux/amd64, linux/arm64)
**Scripts**:
```json
{
"dev": "tsx watch src/index.ts --verbose",
"build": "tsc",
"start": "node dist/index.js",
"test": "node --test tests/**/*.test.js"
}
```
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,758 @@
# Technical Research: Mock GDS MCP Server
**Branch**: `001-mock-gds-server` | **Date**: 2026-04-07
## Phase 0: Technology Decisions
### Decision 1: MCP SDK and Protocol Implementation
**Decision**: Use `@modelcontextprotocol/sdk` official Node.js SDK
**Rationale**:
- Official SDK ensures MCP protocol compliance (Constitution Principle I)
- Handles JSON-RPC 2.0 message format automatically
- Provides TypeScript types for type safety
- Active maintenance by Anthropic
- Simplified tool registration and schema management
**Alternatives Considered**:
- **Custom MCP implementation**: Rejected - high risk of protocol non-compliance, significant development effort
- **Python MCP SDK**: Rejected - requirement specifies Node.js with minimal dependencies
**Implementation Notes**:
- SDK provides `Server` class for initialization
- Tool handlers registered via `server.setRequestHandler`
- Automatic capability negotiation during handshake
- Built-in error handling with standard MCP error codes
### Decision 2: Valkey Client Library
**Decision**: Use `ioredis` v5.x as Valkey client
**Rationale**:
- Valkey is Redis protocol-compatible, ioredis is the most mature Node.js Redis client
- Full support for Redis/Valkey commands (GET, SET, HSET, EXPIRE, etc.)
- Connection pooling and automatic reconnection
- Cluster and sentinel support (for future scaling)
- Pipeline and transaction support
- Active maintenance and TypeScript support
**Alternatives Considered**:
- **node-redis**: Rejected - ioredis has better TypeScript support and more features
- **Custom Valkey protocol**: Rejected - unnecessarily complex
- **No persistence (memory-only)**: Rejected - requirement specifies Valkey for persistence
**Implementation Notes**:
- Use Redis-compatible commands only (avoid Redis-specific extensions)
- Session data stored with TTL (e.g., 1 hour default)
- Key naming: `gds:session:{sessionId}:bookings:{pnr}`
- Use hash structures for complex objects (bookings, searches)
- Enable Valkey RDB/AOF persistence via docker-compose configuration
### Decision 3: Minimal Dependencies Strategy
**Decision**: Limit external dependencies to essentials only
**Core Dependencies** (production):
1. `@modelcontextprotocol/sdk` - MCP protocol (required)
2. `ioredis` - Valkey client (required for persistence)
3. `pino` - Structured logging (minimal, fast, constitution requires observability)
**Development Dependencies**:
- Node.js native test runner (`node:test`) - no jest/mocha overhead
- `c8` - code coverage (lightweight)
**Rationale**:
- Aligns with "minimal libraries" requirement
- Reduces attack surface and dependency maintenance burden
- Faster container builds and smaller images
- Native Node.js features (test runner, assert) are mature and sufficient
**Explicitly Avoided**:
- ❌ Express/Fastify/Koa - MCP uses stdio/SSE, no HTTP server needed
- ❌ TypeORM/Prisma - direct Valkey commands sufficient for key-value storage
- ❌ Jest/Mocha - native test runner adequate
- ❌ Lodash/Ramda - native JS methods sufficient
- ❌ Moment.js/date-fns - native Date and Temporal API (when available)
### Decision 4: Docker Build Strategy
**Decision**: Use Docker Buildx with `docker-bake.hcl` for multi-platform builds
**Rationale**:
- `docker buildx bake` supports complex build configurations
- Multi-platform builds (linux/amd64, linux/arm64) in single command
- Build matrix for multiple tags (latest, version, dev)
- Better caching and parallelization than traditional Dockerfile
- Aligns with requirement specification
**Build Configuration**:
```hcl
# docker-bake.hcl
target "default" {
dockerfile = "docker/Dockerfile"
tags = ["gds-mock-mcp:latest"]
platforms = ["linux/amd64", "linux/arm64"]
cache-from = ["type=registry,ref=gds-mock-mcp:buildcache"]
cache-to = ["type=registry,ref=gds-mock-mcp:buildcache,mode=max"]
}
```
**Dockerfile Strategy**:
- Multi-stage build: builder → production
- Builder stage: install dependencies, run tests
- Production stage: copy only production deps and source
- Use Node.js 20 Alpine for minimal image size
- Non-root user for security
- Health check via MCP ping or valkey connection test
**Alternatives Considered**:
- **Traditional Dockerfile only**: Rejected - buildx bake provides better DX and multi-platform support
- **Docker Compose build**: Rejected - less flexible than buildx, no multi-platform
- **Podman**: Rejected - Docker specified in requirements
### Decision 5: Mock Data Architecture
**Decision**: Embed realistic GDS data in JavaScript modules with deterministic generation
**Data Structure**:
- **Airports**: ~100 major airports with IATA codes (JFK, LAX, ORD, etc.)
- **Airlines**: Major carriers with IATA codes (AA, DL, UA, BA, etc.)
- **Hotels**: 50+ chains/properties across major cities
- **Car Rentals**: Major companies (Hertz, Avis, Enterprise) with vehicle types
- **Flight Routes**: Pre-defined routes with realistic times and prices
- **Pricing Tiers**: Economy ($200-$600 domestic), Business ($800-$2000), First Class ($2500+)
**Generation Strategy**:
- Deterministic: Same search inputs produce same results (for testing reproducibility)
- Controlled Randomness: Optional seed parameter for demo variety
- Rule-Based Pricing: Distance-based pricing with time-of-day adjustments
- Availability Simulation: Random sold-out scenarios (10% of flights)
**Rationale**:
- Embedded data = no external dependencies (fast startup)
- Deterministic = reliable integration tests
- Realistic codes = constitution compliance (Principle II)
- Pre-computed routes = sub-2s response times
**Alternatives Considered**:
- **External API (Skyscanner, etc.)**: Rejected - violates "no external connections" (Constitution Principle III)
- **Database seeding**: Rejected - overhead, embedded data sufficient for mock scope
- **Fully random data**: Rejected - testing requires deterministic outputs
### Decision 6: Testing Strategy
**Decision**: Use Node.js native test runner with three-tier test structure
**Test Tiers**:
1. **Unit Tests**: Individual tool handlers, data generators, validators
- Fast (<100ms total), isolated, no external dependencies
- Mock Valkey client for session tests
2. **Integration Tests**: Full MCP workflows with real Valkey (test container)
- End-to-end booking flows (search → book → retrieve → cancel)
- Multi-service workflows (flight + hotel + car)
- Concurrent session isolation tests
- Use docker-compose test profile for Valkey
3. **Contract Tests**: MCP protocol compliance validation
- Verify JSON-RPC 2.0 format
- Tool schema validation
- Error response structure
**Test Execution**:
```bash
npm test # All tests
npm run test:unit # Fast unit tests only
npm run test:integration # Requires Valkey
npm run test:coverage # Coverage report with c8
```
**Rationale**:
- Native test runner = minimal dependencies
- Three tiers = appropriate test coverage
- Docker test containers = realistic integration tests
- Fast unit tests = quick feedback loop
### Decision 7: Configuration Management
**Decision**: Environment variables with secure defaults
**Configuration Variables**:
```bash
# MCP Server
MCP_TRANSPORT=stdio # stdio or sse
MCP_SESSION_TIMEOUT=3600 # 1 hour session TTL
# Valkey
VALKEY_HOST=localhost
VALKEY_PORT=6379
VALKEY_PASSWORD= # Empty for dev, required for prod
VALKEY_DB=0
VALKEY_KEY_PREFIX=gds:
# Logging
LOG_LEVEL=info # silent, error, warn, info, debug, trace
LOG_PRETTY=false # Pretty print for dev
# Mock Data
MOCK_DATA_SEED=fixed # fixed or random
MOCK_RESPONSE_DELAY=0 # Artificial delay (ms) for demo purposes
```
**Security Defaults**:
- No production credentials in code or .env.example
- Configuration validation on startup
- Reject production-like patterns (Constitution Principle III)
**Rationale**:
- Standard practice for containerized apps
- Easy to override in docker-compose
- Secure defaults prevent accidents
### Decision 8: PNR Generation Strategy
**Decision**: Deterministic PNR generation with TEST prefix
**Format**: `TEST-{BASE32}` where BASE32 is 6 characters
**Example**: `TEST-A1B2C3`
**Generation Algorithm**:
1. Generate session-scoped sequence number
2. Combine with booking timestamp
3. Hash with SHA-256
4. Take first 6 characters of base32 encoding
5. Prefix with "TEST-"
**Rationale**:
- "TEST-" prefix = clear mock indicator (Constitution Principle III)
- Base32 = human-readable, unambiguous (no 0/O, 1/I confusion)
- 6 characters = 1 billion unique combinations (sufficient for testing)
- Deterministic = reproducible test scenarios
- Session-scoped = prevents conflicts
**Alternatives Considered**:
- **Random UUID**: Rejected - too long, not human-friendly
- **Sequential numbers**: Rejected - predictable, not realistic
- **No prefix**: Rejected - violates safety requirement
## Technology Stack Summary
| Component | Technology | Version | Rationale |
|-----------|-----------|---------|-----------|
| Runtime | Node.js | 20 LTS | Current stable, long-term support |
| MCP SDK | @modelcontextprotocol/sdk | Latest | Official SDK, protocol compliance |
| Persistence | Valkey | 8.0+ | Redis-compatible, requirement specified |
| Valkey Client | ioredis | 5.x | Mature, feature-rich, TypeScript support |
| Logging | Pino | Latest | Fast, structured, minimal overhead |
| Testing | node:test | Built-in | Native, zero dependencies |
| Coverage | c8 | Latest | V8 coverage, lightweight |
| Container | Docker | 24+ | Buildx support, multi-platform |
| Orchestration | docker-compose | 2.x | Development environment |
## Performance Considerations
### Expected Performance Profile
- **Search Operations**: <500ms (data generation + Valkey lookup)
- **Booking Operations**: <200ms (validation + Valkey write)
- **Retrieval Operations**: <100ms (Valkey read)
- **Concurrent Sessions**: 50+ (limited by Valkey and Node.js event loop)
- **Memory Footprint**: <100MB per server instance
- **Container Image Size**: <50MB (Alpine-based)
### Optimization Strategies
1. **Caching**: Pre-compute common search results in Valkey
2. **Connection Pooling**: ioredis maintains persistent Valkey connections
3. **Lazy Loading**: Load mock data modules on-demand
4. **Batch Operations**: Use Valkey pipelines for multi-key operations
## Security Considerations
### Mock Data Safety
- ✅ No real API keys or credentials stored
- ✅ Configuration validation rejects production patterns
- ✅ All PNRs prefixed with "TEST-"
- ✅ No external network calls (except to local Valkey)
- ✅ Non-root container user
### Docker Security
- Use official Node.js Alpine base images
- Run as non-root user (node:node)
- Minimal attack surface (no shell, no dev tools in prod image)
- Regular security updates via base image updates
## Open Questions (Resolved)
All technical unknowns from initial planning have been resolved through research above. No blocking issues identified.
## Next Steps
Proceed to Phase 1: Design & Contracts
- Create data-model.md (data structures)
- Define MCP tool contracts in contracts/
- Generate quickstart.md with usage examples
- Update agent context with technology decisions
---
## Remote Access Research (Added 2026-04-07)
This section documents research findings for remote MCP access requirements based on clarifications received after initial planning.
### Decision 8: Streamable HTTP Transport Implementation (MCP Specification Compliant)
**Decision**: Use MCP SDK's `StreamableHTTPServerTransport` over HTTP/1.1 with Server-Sent Events (SSE)
**Question**: How to implement remote transport for MCP SDK per official specification?
**Investigation Findings**:
The MCP specification (2025-11-25) defines **Streamable HTTP** as the standard remote transport. The MCP SDK (@modelcontextprotocol/sdk v1.0.4) provides official transport implementations:
1. **StdioServerTransport** - stdio for local process communication
2. **StreamableHTTPServerTransport** - Remote HTTP/1.1 using SSE (spec-compliant)
3. **WebStandardStreamableHTTPServerTransport** - Platform-agnostic HTTP
4. **SSEServerTransport** - Deprecated legacy transport
**Key Finding**: MCP's **Streamable HTTP** transport uses HTTP/1.1 with Server-Sent Events (SSE), NOT HTTP/2. The specification requires:
- Single endpoint supporting POST, GET, and DELETE methods
- POST for client→server messages (returns SSE stream or 202)
- GET for server→client message stream (optional)
- DELETE for explicit session termination
- SSE for server→client streaming
- Session management via `Mcp-Session-Id` header
- Protocol version via `MCP-Protocol-Version` header (REQUIRED per clarification 2026-04-08)
- Origin header validation for security
- SSE polling pattern: Server sends initial event with ID and empty data, MAY close connection after response, clients reconnect using Last-Event-ID, server sends `retry` field before closing
**Integration Approaches Evaluated**:
**Option A: Native HTTP/1.1 + SSE (MCP Spec Compliant)** ⭐ SELECTED
```
Client → Node.js MCP Server (HTTP/1.1 + SSE via StreamableHTTPServerTransport)
```
- ✅ Direct implementation per MCP specification
- ✅ Zero code complexity - use SDK's `StreamableHTTPServerTransport` directly
- ✅ Single process deployment (no reverse proxy required for spec compliance)
- ✅ Simplified debugging and local development
- ✅ Meets all MCP security requirements (Origin validation, localhost binding)
- ⚠️ Optional: Can add Nginx/Caddy for TLS termination and HTTP/2 upgrade (production enhancement)
**Option B: Reverse Proxy with HTTP/2 Upgrade**
```
Client (HTTP/2) → Nginx/Caddy (HTTP/2 → HTTP/1.1) → Node.js MCP Server (HTTP/1.1+SSE)
```
- ✅ Adds HTTP/2 multiplexing for client connections
- ✅ TLS termination in reverse proxy
- ⚠️ Adds deployment complexity
- ⚠️ Not required for MCP specification compliance
**Rationale for Selection**:
- MCP specification explicitly defines Streamable HTTP as HTTP/1.1 + SSE
- HTTP/2 is an optional enhancement, not a requirement
- Simpler deployment path (single Node.js process)
**Implementation Strategy**:
```javascript
// src/transports/streamable-http.js
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import http from 'node:http';
import { randomUUID } from 'node:crypto';
export class HTTPTransport {
constructor(options = {}) {
this.port = options.port || 3000;
this.host = options.host || '127.0.0.1'; // Localhost for proxy
this.mcpTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: false // Use SSE streaming
});
this.server = http.createServer(async (req, res) => {
// CORS, rate limiting, health check middleware
// Then delegate to mcpTransport.handleRequest()
});
}
}
```
**Docker Configuration**:
```yaml
# docker-compose.yaml
services:
mcp-server:
environment:
TRANSPORT_MODE: http
HTTP_PORT: 3000
HTTP_HOST: 127.0.0.1 # Only accessible via nginx
nginx:
image: nginx:alpine
ports:
- "8080:8080" # External HTTP/2 port
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- mcp-server
```
**Nginx Configuration** (nginx/nginx.conf):
```nginx
server {
listen 8080 ssl http2;
server_name localhost;
ssl_certificate /etc/nginx/certs/cert.pem;
ssl_certificate_key /etc/nginx/certs/key.pem;
location /mcp {
proxy_pass http://mcp-server:3000;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
# SSE support
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
}
location /health {
proxy_pass http://mcp-server:3000/health;
}
}
```
**Alternatives**: Future enhancement can add native HTTP/2 option for single-binary deployment.
---
### Decision 9: MCP Session Management for Remote Access
**Decision**: Stateful sessions with Valkey backing, MCP Session ID mapped to Valkey session
**Question**: How should connection lifecycle and session management work for remote MCP?
**Key Distinction**: HTTP/2 Stream ≠ MCP Session
- **HTTP/2 Stream**: Single request/response cycle, multiplexed over one TCP connection
- **MCP Session**: Persistent identifier (`sessionId`) spanning multiple HTTP requests
- **Valkey Session**: Business logic session tracking PNRs, searches, user context
**Session Lifecycle**:
1. **Initialize**: Client connects → Transport generates sessionId (UUID) → Create Valkey session
2. **Active**: Client makes requests with `MCP-Session-ID` header → Refresh session TTL
3. **Idle**: No requests for N minutes → Session remains in Valkey (TTL not expired)
4. **Expired**: TTL reaches zero → Valkey auto-deletes session data
5. **Reconnect**: Client can resume with same `MCP-Session-ID` header
**Storage Pattern**:
```
session:{sessionId}:metadata → { createdAt, lastActivityAt, transportType, remoteIP }
session:{sessionId}:searches → Recent search results (optional caching)
session:{sessionId}:pnrs → Set of PNR codes created in this session
pnr:{pnr} → Global PNR storage (not session-namespaced)
```
**Implementation**:
```javascript
// src/session/manager.js
async function handleToolCall(request, sessionId) {
if (!sessionId) throw new Error('Session ID required');
let valkeySession = await sessionManager.getSession(sessionId);
if (!valkeySession) {
valkeySession = await sessionManager.createSession(sessionId);
}
await sessionManager.updateActivity(sessionId);
return await toolHandler(request.params, sessionId);
}
```
**Session Isolation**: Each session has isolated Valkey namespace. PNRs stored globally (separate from sessions).
---
### Decision 10: IP-Based Rate Limiting Algorithm
**Decision**: Sliding Window Counter (Hybrid Approach)
**Question**: What rate limiting algorithm works for IP-based tracking without authentication?
**Algorithms Evaluated**:
1. **Fixed Window**: Simple but has burst problem (200 req in 1 second across window boundary)
2. **Sliding Window Log**: Accurate but high memory (stores timestamp per request)
3. **Sliding Window Counter**: Approximation balancing accuracy and performance ⭐ SELECTED
4. **Token Bucket**: Good for burst allowance but complex state management
**Selected Algorithm**: Sliding Window Counter
- ✅ Prevents large bursts (unlike fixed window)
- ✅ Low memory (2 counters per IP)
- ✅ Simple implementation (no Lua scripts required)
- ✅ Accuracy within 1-2% of perfect sliding window
**Implementation**:
```javascript
// src/remote/ratelimit.js
async function checkRateLimit(clientIP, limit = 100, windowSeconds = 60) {
const now = Math.floor(Date.now() / 1000);
const currentWindow = Math.floor(now / windowSeconds);
const previousWindow = currentWindow - 1;
const currentKey = `ratelimit:${clientIP}:${currentWindow}`;
const previousKey = `ratelimit:${clientIP}:${previousWindow}`;
const [currentCount, previousCount] = await Promise.all([
storage.incr(currentKey),
storage.get(previousKey) || 0
]);
if (currentCount === 1) {
await storage.expire(currentKey, windowSeconds * 2);
}
const elapsedInWindow = now % windowSeconds;
const previousWeight = 1 - (elapsedInWindow / windowSeconds);
const estimatedCount = (previousCount * previousWeight) + currentCount;
if (estimatedCount > limit) {
throw new RateLimitError({ limit, current: Math.floor(estimatedCount) });
}
return { allowed: true, remaining: limit - Math.floor(estimatedCount) };
}
```
**Performance**: ~3 Valkey ops per request, ~100 bytes per IP, within 1-2% of perfect sliding window.
**HTTP Headers**:
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1712486460
Retry-After: 15
```
**Client IP Extraction**:
```javascript
function getClientIP(request) {
const forwarded = request.headers['x-forwarded-for'];
if (forwarded) return forwarded.split(',')[0].trim();
const realIP = request.headers['x-real-ip'];
if (realIP) return realIP;
return request.socket.remoteAddress;
}
```
**Configuration**:
```bash
RATE_LIMIT_ENABLED=true
RATE_LIMIT_PER_MINUTE=100
RATE_LIMIT_WINDOW_SECONDS=60
```
---
### Decision 11: Global PNR Storage with TTL
**Decision**: Global namespace with SETEX, independent PNR lifecycle from sessions
**Question**: How to implement global PNR retrieval across sessions with configurable expiration?
**Requirements**:
1. PNRs globally retrievable (any session can retrieve any PNR)
2. PNRs expire after TTL (default 1 hour)
3. PNR creation session logged but doesn't restrict retrieval
4. Session expiration doesn't delete PNRs
**Storage Pattern**:
**Global PNR** (not session-scoped):
```
pnr:TEST-ABC123 → { pnr, status, segments, passengers, createdAt, expiresAt, creatingSessionId }
```
**Session Reference** (for listBookings tool):
```
session:{sessionId}:pnrs → Set<pnr> // PNRs created in this session
```
**Implementation**:
```javascript
// Create PNR
async function createPNR(pnrData, ttlHours = 1) {
const pnr = generatePNR(); // TEST-XXXXXX
const ttlSeconds = ttlHours * 3600;
const pnrRecord = {
pnr,
status: 'confirmed',
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
creatingSessionId: pnrData.sessionId, // For logging only
...pnrData
};
// Store globally with TTL
await storage.setex(`pnr:${pnr}`, ttlSeconds, JSON.stringify(pnrRecord));
// Add to session's created PNRs list
await storage.sadd(`session:${pnrData.sessionId}:pnrs`, pnr);
return pnrRecord;
}
// Retrieve PNR (global, any session)
async function retrieveBooking({ pnr }, sessionId) {
const pnrData = await storage.get(`pnr:${pnr}`);
if (!pnrData) {
throw new NotFoundError(`PNR ${pnr} not found or expired`);
}
return JSON.parse(pnrData);
}
// List PNRs created in session
async function listBookings({ limit = 10 }, sessionId) {
const pnrCodes = await storage.smembers(`session:${sessionId}:pnrs`);
const pnrs = await Promise.all(
pnrCodes.map(async (code) => {
const data = await storage.get(`pnr:${code}`);
return data ? JSON.parse(data) : null;
})
);
return pnrs.filter(Boolean).slice(0, limit);
}
```
**Edge Cases**:
1. **PNR Expired**: `retrieveBooking` returns "PNR not found or expired"
2. **Session Expires Before PNR**: PNR remains globally retrievable
3. **List After Session Expiry**: Returns empty (session reference deleted)
**Configuration**:
```bash
PNR_TTL_HOURS=1
SESSION_TTL_HOURS=24
```
**Storage Efficiency**: ~2KB per PNR, 1000 PNRs = 2MB, auto-cleanup via Valkey TTL.
---
### Decision 12: CORS Configuration
**Decision**: Permissive Wildcard CORS with Network-Level Access Control
**Question**: How to configure CORS for web-based MCP clients?
**CORS Policy**: `Access-Control-Allow-Origin: *` (wildcard)
**Rationale**:
- ✅ Maximum compatibility - any web client can connect from any domain
- ✅ Simplifies development - no origin whitelist configuration
- ✅ Enables browser tools, Chrome extensions, web IDEs
- ⚠️ Requires network-level security (firewall, VPN, private network)
- ⚠️ Only acceptable for trusted development/testing environments
**Security Implications**:
1. **No Credentials**: Wildcard incompatible with `Access-Control-Allow-Credentials: true` (acceptable - we have no auth)
2. **Public Data**: Any website can make requests (acceptable - test data only)
3. **CSRF Potential**: Limited risk (no authentication, state changes require valid PNR)
4. **Network Security**: Deploy within private networks, use firewall rules
**Implementation**:
```javascript
// src/remote/cors.js
export function applyCORS(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'false');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, MCP-Session-ID');
res.setHeader('Access-Control-Expose-Headers', 'X-RateLimit-Limit, X-RateLimit-Remaining');
res.setHeader('Access-Control-Max-Age', '86400');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return true; // Handled
}
return false; // Continue
}
```
**Nginx Alternative**:
```nginx
location /mcp {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
return 204;
}
add_header Access-Control-Allow-Origin * always;
proxy_pass http://mcp-server:3000;
}
```
**Security Posture**: Wildcard CORS acceptable for mock server because:
1. Contains only test data (no sensitive information)
2. No authentication (no credentials to steal)
3. Network-level access controls provide security boundary
4. Maximizes developer flexibility for ad-hoc tooling
**Configuration**:
```bash
CORS_ENABLED=true
CORS_ORIGINS=*
CORS_MAX_AGE=86400
```
---
## Remote Access Technology Summary
| Component | Technology | Decision |
|-----------|-----------|----------|
| **HTTP/2 Server** | Nginx reverse proxy | Terminate HTTP/2, proxy to HTTP/1.1 |
| **MCP Transport** | StreamableHTTPServerTransport | Over HTTP/1.1 (proxied) |
| **Rate Limiting** | Sliding window counter | Valkey-backed, IP-based |
| **PNR Storage** | Global with TTL | Valkey SETEX, independent lifecycle |
| **CORS** | Wildcard policy | `Access-Control-Allow-Origin: *` |
| **Health Check** | Unauthenticated endpoint | `/health` returning JSON status |
## Environment Variables (Remote Mode)
```bash
# Transport
TRANSPORT_MODE=stdio|http|both
HTTP_PORT=3000
HTTP_HOST=127.0.0.1
# Rate Limiting
RATE_LIMIT_ENABLED=true
RATE_LIMIT_PER_MINUTE=100
RATE_LIMIT_WINDOW_SECONDS=60
# PNR/Session
PNR_TTL_HOURS=1
SESSION_TTL_HOURS=24
# CORS
CORS_ENABLED=true
CORS_ORIGINS=*
CORS_MAX_AGE=86400
```
## Next Steps
All remote access research complete. Proceed to update:
1. ✅ data-model.md - Add RemoteConnection, RateLimitRecord, HealthStatus entities
2. ✅ contracts/ - Add health endpoint contract
3. ✅ quickstart.md - Add remote access setup instructions

View File

@@ -0,0 +1,352 @@
# Feature Specification: Mock GDS MCP Server
**Feature Branch**: `001-mock-gds-server`
**Created**: 2025-01-22
**Status**: Draft
**Input**: User description: "Build a Mock GDS MCP Server"
## Clarifications
### Session 2026-04-07
- Q: Authentication implementation approach for remote connections → A: Don't implement authentication at this time (authentication deferred to future version, remote access will be open/unauthenticated for initial implementation)
- Q: Remote transport protocol selection → A: HTTP/2 with streaming (modern HTTP version with multiplexed streams for efficient concurrent request handling)
- Q: Session expiry and PNR lifecycle behavior → A: Global with TTL - PNRs are globally retrievable for a configurable time period (e.g., 1 hour) after creation, regardless of which session created them, allowing cross-session retrieval for testing flexibility
- Q: CORS policy configuration for web clients → A: Allow all origins (*) - Permissive wildcard CORS policy allowing any web client to connect, suitable for open development/testing environments
- Q: Rate limiting identifier without authentication → A: Client IP address - Track request rates per source IP address to prevent abuse while supporting unauthenticated access
### Session 2026-04-08
- Q: MCP transport protocol implementation - correct specification compliance → A: Implement MCP Streamable HTTP spec (HTTP/1.1 + SSE) - Follow the official MCP 2025-11-25 specification: HTTP/1.1 with Server-Sent Events, single /mcp endpoint with POST/GET/DELETE methods, event IDs for resumability, MCP-Protocol-Version header handling. Optional Nginx reverse proxy can upgrade to HTTP/2 for clients while proxying HTTP/1.1 to the application.
- Q: SSE connection management strategy for resource efficiency → A: Implement SSE polling pattern with connection closure - Server sends initial event with ID and empty data to prime reconnection, then MAY close the HTTP connection (not stream) after sending responses to reduce resource usage. Clients reconnect using Last-Event-ID header to resume. Server sends retry field before closing connections per MCP 2025-11-25 specification.
- Q: MCP-Protocol-Version header validation behavior → A: Require MCP-Protocol-Version header on all requests - Reject requests without the header with 400 Bad Request immediately, forcing all clients to send version explicitly with no backward compatibility fallback.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Flight Search and Booking (Priority: P1)
A developer building a travel application needs to test flight search and booking functionality. They initiate a flight search for a specific route and date, receive mock availability results with realistic pricing, select a flight, and complete a booking that returns a PNR (Passenger Name Record). The mock server provides immediate responses with consistent data, allowing the developer to verify their application's search UI, results display, booking form, and confirmation workflow.
**Why this priority**: Flight booking is the core GDS capability and most commonly used. Without this, the mock server delivers no meaningful value. This represents the essential MVP - a working flight search and book operation demonstrates the server is functional.
**Independent Test**: Can be fully tested by executing a flight search request through MCP, receiving mock flight results, submitting a booking request with passenger details, and receiving a valid PNR. Delivers immediate value for testing basic travel app workflows without requiring any other travel service types.
**Acceptance Scenarios**:
1. **Given** a developer requests flights from JFK to LAX on a future date, **When** the search executes, **Then** the system returns 3-5 mock flight options with different times, airlines (using valid IATA codes like AA, DL, UA), realistic prices ($200-$800), and available seats
2. **Given** search results are displayed, **When** the developer selects a specific flight and provides passenger details (name, contact info), **Then** the system creates a booking and returns a 6-character alphanumeric PNR (e.g., ABC123)
3. **Given** a valid PNR exists, **When** the developer retrieves the booking, **Then** the system returns complete booking details including passenger information, flight details, booking status, and total price
4. **Given** a confirmed booking with PNR, **When** the developer requests cancellation, **Then** the system marks the booking as cancelled and confirms the cancellation
---
### User Story 2 - Hotel Search and Multi-Service Bundling (Priority: P2)
A developer needs to test a travel package feature that combines flights and hotels. They search for hotels in the destination city for specific dates, view available properties with realistic amenities and pricing, add a hotel to an existing flight booking to create a multi-service itinerary, and retrieve the combined booking showing both flight and hotel details under one PNR.
**Why this priority**: Multi-service bookings are a common real-world scenario and differentiate GDS from simple flight APIs. This adds significant testing value but flight-only booking (P1) must work first. Can be developed after P1 is stable.
**Independent Test**: Can be tested by performing hotel searches independently, creating hotel-only bookings, and then testing the bundling workflow by adding hotels to existing flight bookings. Demonstrates the server can handle multiple service types and maintain complex booking state.
**Acceptance Scenarios**:
1. **Given** a destination city code (e.g., LAX) and date range, **When** the developer searches for hotels, **Then** the system returns 5-10 mock properties with names, star ratings (2-5 stars), nightly rates ($80-$400), amenities lists, and availability
2. **Given** an existing flight booking PNR, **When** the developer adds a hotel to the itinerary with check-in/check-out dates matching travel dates, **Then** the system updates the PNR to include both flight and hotel segments
3. **Given** a multi-service booking, **When** the developer retrieves the PNR, **Then** the system returns a complete itinerary showing flight departure/arrival times, hotel check-in/check-out dates, total price breakdown by service type, and overall booking status
---
### User Story 3 - Car Rental and Complete Travel Package (Priority: P3)
A developer testing a comprehensive travel platform needs to create complete travel packages including flights, hotels, and car rentals. They search for rental cars at the destination airport, view available vehicle classes with daily rates, add a car rental to an existing multi-service booking, and manage a complete end-to-end travel itinerary under one PNR.
**Why this priority**: Car rentals complete the full travel package offering but are less commonly used than flights and hotels. This is valuable for comprehensive testing but not essential for initial MVP. Requires P1 and P2 to be working first.
**Independent Test**: Can be tested by performing car rental searches independently, creating car-only bookings, and integrating with flight+hotel bookings. Demonstrates full GDS capability coverage and complex multi-segment itinerary management.
**Acceptance Scenarios**:
1. **Given** an airport code (e.g., LAX) and rental dates, **When** the developer searches for cars, **Then** the system returns 4-6 vehicle options (economy, compact, SUV, luxury) with daily rates ($35-$150), rental companies (using valid codes like Hertz, Avis, Enterprise), and availability
2. **Given** an existing flight+hotel booking, **When** the developer adds a car rental with pickup/dropoff dates matching the travel period, **Then** the system updates the PNR to include flight, hotel, and car segments in chronological order
3. **Given** a complete travel package, **When** the developer requests the itinerary, **Then** the system returns a structured view showing the full trip timeline: flight arrival, car pickup, hotel check-in, hotel check-out, car dropoff, and return flight
---
### User Story 4 - Session Management for Concurrent Testing (Priority: P2)
A QA team runs automated integration tests that execute multiple booking scenarios simultaneously. Each test session needs isolated booking state so concurrent tests don't interfere with each other. The mock server assigns unique session identifiers, maintains separate booking contexts per session, and ensures that searches, bookings, and retrievals in one session are completely isolated from other sessions.
**Why this priority**: Essential for realistic testing environments where multiple developers or automated tests run in parallel. Without session isolation, the mock server can only handle one user at a time, severely limiting its usefulness. This is critical for CI/CD integration.
**Independent Test**: Can be tested by initiating 5-10 concurrent MCP sessions, performing different booking operations in each (different routes, dates, passenger names), and verifying that retrieving a PNR in one session never returns data from another session. Confirms multi-user readiness.
**Acceptance Scenarios**:
1. **Given** two developers start separate MCP sessions, **When** each performs a flight search with different criteria (different cities and dates), **Then** each receives results matching their search only, with no data leakage between sessions
2. **Given** concurrent test automation runs 10 booking flows simultaneously, **When** each creates a booking with unique passenger names, **Then** the system generates 10 distinct PNRs and each can be retrieved with correct passenger information in its originating session
3. **Given** a session with multiple active bookings, **When** the developer ends the session, **Then** the system maintains booking data for retrieval operations but prevents new modifications, allowing tests to verify final state
---
### User Story 6 - Remote Access for Distributed Teams (Priority: P2)
A distributed development team needs to access the mock GDS server from different locations without setting up local instances. Remote developers connect to a centrally hosted mock server over the internet and execute booking workflows from their local development environments or web-based tools. The remote server handles multiple concurrent connections while maintaining session isolation.
**Why this priority**: Enables distributed teams to share a single mock server instance, reducing setup overhead and ensuring consistency across team members. Remote access supports cloud-based development environments, CI/CD pipelines running in the cloud, and web-based MCP clients that cannot use local stdio connections.
**Independent Test**: Can be tested by deploying the server to a public endpoint, connecting from multiple external clients, executing parallel booking workflows, and verifying that each session maintains proper isolation and receives correct responses. Confirms remote multi-user capability.
**Acceptance Scenarios**:
1. **Given** a remote MCP client, **When** the client connects to the server's public endpoint, **Then** the system establishes a connection and responds to MCP tool calls with the same functionality as stdio mode
2. **Given** web-based MCP clients from different origins, **When** clients connect to the server, **Then** the system allows connections and handles cross-origin requests appropriately
3. **Given** multiple remote clients connected simultaneously, **When** each executes independent booking workflows, **Then** the system maintains separate session contexts and prevents data leakage between remote sessions
4. **Given** monitoring tools checking server availability, **When** health check endpoints are queried, **Then** the system returns status information indicating operational readiness
---
### User Story 5 - Realistic Test Data for Demonstrations (Priority: P3)
A sales team demonstrates travel booking software to potential customers. They need realistic, professional-looking mock data that appears authentic during presentations - valid airline names and codes, recognizable hotel chains, realistic pricing that matches market expectations, and diverse route options covering major travel markets (domestic US, transatlantic, Asia-Pacific). The mock server provides curated demonstration scenarios with high-quality data suitable for live demos.
**Why this priority**: Important for sales and training but not for development testing. Demo quality data enhances presentations but basic valid mock data (covered in P1) suffices for testing. This is a polish feature that improves adoption but isn't technically essential.
**Independent Test**: Can be tested by reviewing the mock data catalog, executing searches for major routes (NYC-LAX, NYC-LON, LAX-TYO), verifying that returned data includes recognizable brands (United, Marriott, Hertz), realistic prices (not $1 or $999999), and appropriate travel times/durations. Success means demo-ready output requiring no explanation or apology.
**Acceptance Scenarios**:
1. **Given** a demo scenario for domestic US travel, **When** the sales person searches JFK to LAX, **Then** the system returns flights on American, Delta, and United with prices between $250-$600, realistic departure times (6am, 9am, 2pm, 5pm), and 5.5-6 hour flight durations
2. **Given** a demo scenario for international travel, **When** the sales person searches NYC to London, **Then** the system returns overnight flights on major carriers (BA, AA, Virgin Atlantic) with prices $800-$1500, premium cabin options (economy, business, first class), and realistic 7-8 hour flight times
3. **Given** a demo for hotel bookings, **When** searching major cities (New York, London, Tokyo), **Then** the system returns recognizable hotel brands (Marriott, Hilton, Hyatt) with appropriate pricing tiers ($150-$300 mid-range, $400-$800 luxury) and realistic amenities (WiFi, breakfast, gym, pool)
---
### Edge Cases
- **Empty Search Results**: What happens when a search request uses invalid codes (non-existent airport ZZZZ, impossible date like Feb 30)? System should return user-friendly error messages with specific reasons ("Airport code ZZZZ not recognized in mock database", "Invalid date format") rather than crashing.
- **Booking Modifications**: How does the system handle attempts to modify an already-cancelled booking? System must reject modification requests and return clear error indicating booking is in cancelled state and cannot be changed.
- **Session Expiry**: PNRs remain globally retrievable for a configurable time period (default 1 hour) after creation, regardless of which session created them or whether that session is still active. After the TTL expires, PNR retrieval returns "PNR not found or expired" error. This allows testing scenarios where bookings need to be retrieved across different sessions or after reconnection.
- **Incomplete Booking Data**: How does system respond when booking request is missing required fields (no passenger name, no contact info)? System must validate required fields and return specific validation errors ("Missing required field: passenger.lastName", "Missing required field: contact.email").
- **Concurrent Booking Same Flight**: What happens when two sessions simultaneously book the last seat on a mock flight? System should handle this realistically - either both succeed (infinite mock inventory), or implement basic availability tracking and one booking succeeds while other receives "No availability" response.
- **Invalid PNR Format**: How does retrieval handle malformed PNRs (wrong length, invalid characters like "A@B!C1")? System should validate PNR format and return "Invalid PNR format" error rather than attempting database lookup.
- **Date Logic**: What happens when hotel check-out date is before check-in date, or car dropoff is before pickup? System must validate date sequences and return errors like "Check-out date must be after check-in date".
- **Multi-Service Timing Conflicts**: What happens when developer tries to add a hotel with dates that don't overlap with the flight dates in the same booking (flight on June 10, hotel June 20-22)? System should either warn about date mismatch or enforce logical consistency (hotel dates must overlap with travel period).
### Remote Access Edge Cases
- **Rapid Connection Attempts**: What happens when a client attempts to connect 20 times in quick succession (potential connection retry loop)? System should handle connection rate limiting separately from request rate limiting, allowing reasonable reconnection attempts but preventing connection floods.
- **CORS Preflight Failures**: What happens when a web-based client from an allowed origin sends preflight OPTIONS requests? System must respond correctly to preflight requests to enable web clients to complete connection establishment.
- **Concurrent Requests from Same Remote Session**: What happens when a remote client sends 10 simultaneous booking requests in parallel using the same session? System should handle concurrent requests within a session appropriately - either process them in parallel maintaining consistency, or serialize them with appropriate queuing.
- **Health Check During High Load**: What happens when health check endpoint is queried while server is processing maximum concurrent connections? Health check should remain responsive (sub-second response) even under load, possibly with degraded status if resource utilization is high.
- **Rate Limit Edge Conditions**: What happens when a client makes exactly the limit number of requests at the boundary of a time window? System should handle edge cases consistently (requests at exactly the limit boundary should either all succeed or clearly fail with limit message, not random behavior).
- **Connection Cleanup on Client Crash**: What happens when a remote client crashes or loses network connectivity without gracefully closing the connection? System should detect inactive connections (no requests for N minutes) and clean up associated resources, freeing session data for garbage collection.
- **Invalid Origin Headers**: What happens when a remote client sends requests with spoofed or missing Origin headers? System should validate CORS based on request characteristics and either allow (if origin validation passes) or deny with appropriate CORS error.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST expose all travel search and booking capabilities as MCP tools with clearly defined input parameters and output schemas
- **FR-002**: System MUST support flight search with parameters: origin airport code (3-letter IATA), destination airport code, departure date, optional return date, and passenger count, returning a list of available flights with times, airlines, prices, and seat availability
- **FR-003**: System MUST support hotel search with parameters: city or airport code, check-in date, check-out date, and guest count, returning a list of available properties with names, ratings, nightly rates, total prices, and amenities
- **FR-004**: System MUST support car rental search with parameters: pickup location (airport code), pickup date/time, dropoff location, dropoff date/time, returning available vehicle classes with daily rates, rental companies, and vehicle specifications
- **FR-005**: System MUST support booking creation for any travel service (flight, hotel, car) with passenger/guest details, payment method placeholder (no actual processing), and return a unique PNR in format TEST-{6-character alphanumeric} (e.g., TEST-A1B2C3)
- **FR-006**: System MUST allow multiple service types (flight + hotel + car) to be combined under a single PNR, maintaining all segment details in one unified booking record
- **FR-007**: System MUST support PNR retrieval by PNR code, returning complete booking details including all service segments, passenger information, prices, booking status, and creation timestamp
- **FR-008**: System MUST support booking cancellation by PNR, updating booking status to "cancelled" and returning cancellation confirmation with timestamp
- **FR-009**: System MUST maintain session isolation - each MCP session receives a unique session identifier and all bookings, searches, and state are isolated per session with no data leakage between concurrent sessions
- **FR-010**: System MUST include realistic mock data covering at minimum: 20+ major airports (US domestic and international hubs), 15+ airlines with valid IATA codes, 30+ hotels across major cities, and 5+ car rental companies
- **FR-011**: System MUST use only valid IATA/ICAO codes for airports and airlines - no fictional codes that would confuse users testing against real GDS documentation
- **FR-012**: System MUST clearly mark all data as mock/test data in responses (via metadata field like "data_source": "mock") to prevent any confusion with production systems
- **FR-013**: System MUST log all operations (search, book, retrieve, cancel) with timestamps, session IDs, and operation parameters to support debugging and test verification
- **FR-014**: System MUST return structured error responses with error codes and human-readable messages for all error conditions (invalid input, not found, validation failures)
- **FR-015**: System MUST support common GDS workflows including: price verification (confirm price before booking), seat/room availability checks, and multi-step booking processes (search → select → confirm)
- **FR-016**: System MUST generate realistic pricing based on route distance, service class, and time factors - no arbitrary prices like $1 or $999999
- **FR-017**: System MUST maintain booking state persistence with configurable TTL (default 1 hour) - bookings remain globally retrievable from any session until TTL expiration, after which they return "not found" errors
- **FR-018**: System MUST validate all input parameters (date formats YYYY-MM-DD, valid airport codes, positive passenger counts, logical date sequences) and reject invalid requests with specific error messages
- **FR-019**: System MUST support date/time handling for different time zones - flight times in local airport time, with UTC timestamps in responses for unambiguous time tracking
- **FR-020**: System MUST prevent any connection attempts to real booking systems, payment processors, or external GDS providers - all operations are fully self-contained mock operations
### Remote Access Functional Requirements
- **FR-021**: System MUST support both stdio transport (for local MCP clients) and remote transport (for internet-accessible MCP clients), with transport mode configurable via environment settings
- **FR-022**: System MUST implement MCP Streamable HTTP transport per specification 2025-11-25: HTTP/1.1 with Server-Sent Events (SSE), single /mcp endpoint supporting POST (client-to-server messages with SSE response), GET (server-to-client message stream), and DELETE (session termination) methods, event IDs for stream resumability, MCP-Protocol-Version header handling, and MCP-Session-Id header for session management. The application uses HTTP/1.1 + SSE; optional reverse proxy (Nginx/Caddy) may upgrade client connections to HTTP/2 while proxying HTTP/1.1 to the application.
- **FR-022a**: System MUST implement SSE polling pattern for resource efficiency: send initial event with ID and empty data field to prime client reconnection, MAY close HTTP connection (not stream) after sending responses to reduce resource usage, clients reconnect using Last-Event-ID header to resume stream, and server MUST send retry field (milliseconds) before closing connections to guide client reconnection timing per MCP specification.
- **FR-022b**: System MUST require MCP-Protocol-Version header on all remote HTTP requests and reject requests without this header with HTTP 400 Bad Request and error message indicating the missing required header. System MUST validate the header value and support protocol version 2025-11-25, rejecting unsupported versions with HTTP 400 Bad Request.
- **FR-023**: System MAY implement authentication for remote connections in future versions, but initial implementation allows unauthenticated access suitable for trusted development/testing environments
- **FR-024**: System MUST support cross-origin requests from web-based MCP clients using permissive CORS policy (Allow-Origin: *), enabling browser-based tools and applications from any domain to connect to the remote server
- **FR-025**: System MUST enforce rate limiting on remote connections based on client IP address to prevent abuse, with configurable limits for requests per IP per time period (e.g., 100 requests per minute per IP address)
- **FR-026**: System MUST validate all input data from remote connections with security checks protecting against malicious payloads or injection attacks
- **FR-027**: System MUST provide health check endpoints that return operational status without requiring authentication, enabling monitoring systems and load balancers to verify server availability
- **FR-028**: System MUST maintain session isolation for remote clients identical to stdio sessions - each remote connection receives a unique session identifier with complete data isolation from other remote and local sessions
- **FR-029**: System MUST handle multiple concurrent remote client connections (minimum 50 simultaneous remote sessions) while maintaining performance and isolation guarantees
- **FR-030**: System MUST support secure connections using standard encryption for remote transport, protecting data in transit from client to server
- **FR-031**: System MUST log connection events and rate limit violations for remote connections in addition to standard operation logging, supporting operational monitoring
- **FR-032**: System MUST allow transport mode to be selected at server startup (stdio-only, remote-only, or both) to support different deployment scenarios from local development to public cloud hosting
- **FR-033**: System MUST gracefully handle remote client disconnections, cleaning up session resources and maintaining data consistency even when clients disconnect unexpectedly
- **FR-034**: System MUST provide the same MCP tool functionality and response format for both stdio and remote connections, ensuring consistent behavior regardless of transport method
### Key Entities
- **Flight Segment**: Represents one flight leg with departure/arrival airports (IATA codes), departure/arrival times (local time + UTC), airline code, flight number, aircraft type, duration, available fare classes (economy/business/first), pricing per class, and seat availability counts
- **Hotel Reservation**: Represents accommodation booking with property name, location (city/address), star rating (1-5), check-in/check-out dates, room type (single/double/suite), nightly rate, total price, amenities list (WiFi, parking, breakfast, gym, pool), and cancellation policy
- **Car Rental**: Represents vehicle rental with pickup/dropoff locations (airport codes or city), pickup/dropoff date-times, rental company name and code, vehicle class (economy/compact/SUV/luxury), daily rate, total price, vehicle specifications (make/model/seats), and mileage policy (unlimited/limited)
- **Passenger/Guest**: Represents traveler information with full name (first/middle/last), date of birth, contact details (email, phone), loyalty program numbers (frequent flyer, hotel rewards), and special requests/needs (meal preferences, accessibility requirements, seat preferences)
- **PNR (Passenger Name Record)**: Core booking entity containing unique 6-character alphanumeric identifier, booking status (confirmed/cancelled/pending), creation timestamp, expiration timestamp (creation time + configurable TTL, default 1 hour), list of all service segments (flights, hotels, cars), passenger/guest details, total price summary, payment method placeholder, and originating session identifier for logging purposes. PNRs are globally retrievable from any session until TTL expiration.
- **Session**: Represents an MCP connection session with unique session ID, creation timestamp, session expiry time, and isolation boundary ensuring no cross-session data access during operation. Sessions track which PNRs were created during the session for logging but do not restrict retrieval - PNRs remain accessible globally based on their own TTL.
- **Search Query**: Represents search parameters for any service type with service type (flight/hotel/car), origin/destination or location, dates, passenger/guest counts, optional filters (price range, star rating, airline preference), and returns matching results list
- **Mock Data Record**: Represents static test data entries for airports (code, city, country, timezone), airlines (code, name, country), hotels (name, location, rating, base rates), and rental companies (name, code, vehicle inventory), all marked with "mock: true" flag
- **Remote Connection**: Represents an active remote client connection with connection identifier, remote client IP address (for logging and rate limiting), connection start timestamp, last activity timestamp, transport protocol details, and associated session identifier
- **Rate Limit Record**: Tracks request rates per client IP address with IP address identifier, time window start, request count in current window, limit threshold, and next reset timestamp, enabling enforcement of usage limits to prevent abuse from individual source addresses
- **Health Status**: Represents server operational status with overall health state (healthy/degraded/unhealthy), active connection counts (stdio and remote), resource utilization metrics, uptime duration, and any current issues or warnings
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Developers can complete a full flight search and booking workflow (search → select → book → retrieve) in under 30 seconds with clear, understandable responses at each step
- **SC-002**: System handles 50 concurrent MCP sessions executing independent booking workflows simultaneously without response degradation, data corruption, or cross-session data leakage
- **SC-003**: All search operations (flight, hotel, car) return results in under 2 seconds with realistic data, enabling developers to build responsive user interfaces without waiting for slow mock responses
- **SC-004**: 100% of mock data uses valid IATA/ICAO codes and realistic pricing within market ranges (flights $100-$2000, hotels $50-$800/night, cars $25-$200/day) so developers don't need to question data authenticity
- **SC-005**: Integration tests using the mock server can verify end-to-end booking workflows without manual intervention, achieving 95% test automation coverage for travel app booking features
- **SC-006**: Sales demonstrations using the mock server require zero data explanation or disclaimer - all returned data appears professional and realistic enough to represent production-quality responses
- **SC-007**: New developers unfamiliar with GDS concepts can understand booking workflows by examining mock server responses within 15 minutes, using the clear response structure and realistic data as learning material
- **SC-008**: System logs provide sufficient detail that 90% of failed test cases can be debugged by examining logs alone, without needing to instrument additional debugging code
- **SC-009**: Zero production system connections or external API calls are made during any operation - 100% self-contained mock operation with no risk of accidental real bookings
- **SC-010**: Error messages enable developers to fix invalid requests on first attempt - each validation error specifies the exact field, expected format, and current invalid value
### Remote Access Success Criteria
- **SC-011**: Remote developers can connect to a deployed server instance from any location with internet access and complete booking workflows with the same sub-30-second performance as local stdio connections
- **SC-012**: System handles 50 concurrent remote clients executing independent booking workflows simultaneously while maintaining sub-2-second response times and complete session isolation
- **SC-013**: Web-based MCP clients from different domains can successfully connect and execute MCP tools without CORS-related failures, enabling browser-based development tools
- **SC-014**: Rate limiting prevents abuse while allowing legitimate usage - clients can execute at least 100 requests per minute under normal operation, with clear error messages when limits are exceeded
- **SC-015**: Health check endpoints respond within 500ms and accurately reflect server operational status, enabling effective monitoring and automated health checks by deployment platforms
- **SC-016**: Distributed teams can share a single remote server instance without coordination overhead - each developer receives isolated session data regardless of how many other team members are connected
- **SC-017**: Remote connections survive typical network interruptions - temporary disconnections of under 30 seconds result in automatic reconnection without data loss or session corruption
- **SC-018**: Transport mode configuration is clear and validated at startup - invalid configuration (missing required settings, contradictory options) results in immediate startup failure with specific error messages indicating the configuration problem
## Assumptions
- **Mock Data Scope**: The mock database will include major US airports, international hubs (London, Paris, Tokyo, Dubai), and popular tourist destinations, but will not attempt to be comprehensive (all 40,000+ airports worldwide). Coverage focuses on common testing scenarios.
- **Pricing Realism**: Mock prices will be realistic based on current market ranges but will not update for inflation, seasonal changes, or market conditions. Fixed pricing tiers will be maintained for consistency across tests and demos.
- **No Payment Processing**: All payment operations are placeholders - the system accepts payment method information (credit card type, last 4 digits) for workflow testing but performs no validation, tokenization, or processing of any kind.
- **Session Duration**: Sessions remain active for the duration of the MCP connection. When the connection closes, session metadata may be cleaned up, but PNRs created during that session remain retrievable globally based on their own TTL (default 1 hour from creation). This allows test scenarios to create bookings in one session and verify them in another.
- **Infinite Inventory**: Mock availability is effectively unlimited - any search returns available results, and any booking succeeds (unless specifically testing error scenarios). No real inventory tracking or "sold out" conditions unless explicitly requested in test scenario.
- **Simplified Fare Rules**: Complex GDS fare rules (change fees, blackout dates, minimum stay requirements) are simplified or omitted. The focus is on basic booking workflows, not exhaustive fare rule validation.
- **English Language Only**: All mock data (airport names, hotel names, city names) will be in English with standard Latin character encoding (UTF-8). International character support is out of scope for v1.
- **No Real-Time Updates**: Prices and availability are static mock data. There are no real-time updates, dynamic pricing, or availability changes based on time or demand. Each search returns consistent results for the same parameters.
- **MCP Protocol Version**: System assumes MCP version 1.0 protocol standards for tool definition, parameter passing, and response formatting. Compatibility with future MCP versions may require updates.
- **Target Users**: Primary users are software developers and QA engineers familiar with travel industry concepts (IATA codes, PNR terminology). The system is not designed for end-user consumer access.
- **No Authentication**: Session isolation is based on MCP connection identity, not user authentication. There is no login system, user accounts, or permission management. All sessions have equal access to all mock operations.
- **Development Environment**: The mock server is designed for development, testing, and demonstration environments. It is not intended for production use, load testing beyond moderate concurrency (50-100 sessions), or as a production GDS proxy.
### Remote Access Assumptions
- **Network Environment**: Remote deployments assume standard cloud hosting environments with public internet access and no unusual firewall restrictions. Complex enterprise network configurations (corporate proxies, VPNs, non-standard ports) may require additional configuration.
- **MCP Transport Protocol**: System implements MCP Streamable HTTP transport per specification 2025-11-25 using HTTP/1.1 with Server-Sent Events (SSE). Optional reverse proxy (Nginx, Caddy) may be deployed to upgrade client connections to HTTP/2 while proxying HTTP/1.1 to the application, but this is an infrastructure enhancement not required for MCP specification compliance.
- **No Authentication (Initial Version)**: Initial implementation provides open/unauthenticated remote access suitable for trusted development/testing environments. Authentication may be added in future versions when deployment to less trusted environments is required.
- **Rate Limiting Scope**: Rate limits are designed to prevent abuse and accidental infinite loops, not to implement billing tiers or usage tracking. Limits are set at reasonable levels for development usage (hundreds of requests per minute) and may be adjusted based on deployment capacity. Rate limiting may be based on client IP address or connection identifier rather than authenticated credentials.
- **Security Posture**: Remote access is appropriate for development/testing environments with trusted team members on trusted networks. Initial version without authentication is NOT designed for public internet exposure to untrusted users. Deployment should use network-level access controls (firewalls, VPNs, private networks) to limit access to authorized development teams.
- **CORS Configuration**: CORS support uses permissive wildcard policy (Access-Control-Allow-Origin: *) allowing connections from any web origin. This maximizes development flexibility - web-based tools, browser extensions, and client applications from any domain can connect without CORS configuration. Suitable for trusted development/testing networks where access control is managed at the network layer (firewalls, VPNs) rather than application layer.
- **Connection Persistence**: Remote connections are expected to be relatively short-lived (minutes to hours, not days). Long-lived connections are supported but may be subject to cleanup on server restart or maintenance. No connection migration or state persistence across server restarts.
- **Health Check Usage**: Health checks are designed for basic operational monitoring and load balancer integration. They report simple up/down status and basic metrics, not comprehensive performance monitoring or detailed diagnostics.
- **Transport Coexistence**: When both stdio and remote transport are enabled simultaneously, they share the same mock data and session isolation boundaries but operate independently. No cross-transport session sharing or migration.
- **Deployment Model**: Remote deployments assume container-based hosting (Docker) or similar cloud deployment models. The system does not include installation packages, system service configurations, or traditional server deployment tooling.
- **SSL/TLS Termination**: Secure connections assume standard SSL/TLS termination at the server or load balancer level. The mock server itself focuses on application-level functionality and relies on deployment infrastructure for transport security.
- **Monitoring Integration**: Basic health endpoints are provided, but comprehensive monitoring (metrics collection, distributed tracing, log aggregation) relies on standard deployment infrastructure and observability tools, not custom mock server monitoring features.

View File

@@ -0,0 +1,549 @@
# Tasks: Mock GDS MCP Server
**Input**: Design documents from `/specs/001-mock-gds-server/`
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/ ✅
**Tests**: Tests are NOT explicitly requested in the specification, so test tasks are OMITTED per template guidelines.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3, US4, US5)
- All tasks include exact file paths
---
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Project initialization and basic structure from plan.md
- [X] T001 Initialize Node.js 20 project with package.json including @modelcontextprotocol/sdk, ioredis, pino dependencies
- [X] T002 Create project directory structure: src/{tools,data,session,validation,utils}/, tests/{integration,unit,fixtures}/, docker/
- [X] T003 [P] Configure ESLint and Prettier for code quality in .eslintrc.json and .prettierrc
- [X] T004 [P] Create .dockerignore and .gitignore files for build optimization
- [X] T005 [P] Create docker-compose.yaml with Valkey service configuration (port 6379, persistence enabled)
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
- [X] T006 Implement Pino structured logger setup in src/utils/logger.js with configurable log levels
- [X] T007 [P] Implement error handling utilities in src/utils/errors.js with MCP error codes
- [X] T008 [P] Create Valkey client wrapper in src/session/storage.js with connection pooling and error handling
- [X] T009 [P] Implement JSON schema validators in src/validation/validators.js using native validation
- [X] T010 [P] Create MCP tool schemas in src/validation/schemas.js based on contracts/mcp-tools.md
- [X] T011 Create session lifecycle manager in src/session/manager.js with TTL management (1 hour default)
- [X] T012 [P] Implement PNR generation utilities in src/data/pnr.js with TEST- prefix and base32 encoding
- [X] T013 Initialize MCP server in src/server.js with @modelcontextprotocol/sdk Server class
- [X] T014 Create MCP server entry point in src/index.js with stdio transport and error handling
**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
---
## Phase 3: User Story 1 - Flight Search and Booking (Priority: P1) 🎯 MVP
**Goal**: Enable developers to test flight search and booking functionality with realistic mock data. Search flights, create bookings, retrieve bookings, and cancel bookings.
**Independent Test**: Execute searchFlights MCP tool for JFK→LAX route, receive 3-5 mock flight results with valid IATA codes and realistic prices, create booking with passenger details, receive TEST- prefixed PNR, retrieve booking by PNR, and cancel booking. All operations complete successfully without requiring any other service types.
### Core Data for User Story 1
- [X] T015 [P] [US1] Create airports mock data in src/data/airports.js with 100+ major airports (IATA codes, names, cities, timezones, coordinates)
- [X] T016 [P] [US1] Create airlines mock data in src/data/airlines.js with 30+ carriers (IATA codes, names, countries)
- [X] T017 [US1] Implement flight data generator in src/data/flights.js with deterministic pricing, duration calculation, and availability logic
### Flight Search Tool
- [X] T018 [US1] Implement searchFlights tool handler in src/tools/flights.js with input validation (origin, destination, departureDate, passengers, cabin)
- [X] T019 [US1] Add flight search result generation with 3-5 mock flights per search, realistic schedules (6am-10pm departures), and price ranges ($200-$800 economy, $800-$2000 business, $2500+ first)
- [X] T020 [US1] Implement seat availability simulation (90% available, 10% sold out) and booking class assignment. **Note**: Implements infinite inventory model where concurrent bookings on same flight both succeed. This aligns with spec.md edge case discussion and is appropriate for mock server testing scope.
### Flight Booking Tool
- [X] T021 [US1] Implement bookFlight tool handler in src/tools/flights.js with passenger validation (firstName, lastName, email, phone)
- [X] T022 [US1] Add flight booking creation logic: validate flight selection, generate PNR via src/data/pnr.js, persist to Valkey with key gds:session:{sessionId}:booking:{pnr}
- [X] T023 [US1] Implement PNR storage structure in Valkey with FlightSegment, Passenger, pricing, status fields per data-model.md
- [X] T024 [US1] Add session booking tracking: update gds:session:{sessionId}:bookings set and increment bookingCount
### Booking Management Tools
- [X] T025 [US1] Implement retrieveBooking tool handler in src/tools/bookings.js with PNR validation and Valkey lookup
- [X] T026 [US1] Add booking retrieval logic: fetch from gds:session:{sessionId}:booking:{pnr}, return complete booking details with all segments
- [X] T027 [US1] Implement cancelBooking tool handler in src/tools/bookings.js with status transition validation (confirmed→cancelled only)
- [X] T028 [US1] Add cancellation logic: update booking status to 'cancelled', persist timestamp, return confirmation
### MCP Server Integration
- [X] T029 [US1] Register searchFlights, bookFlight, retrieveBooking, cancelBooking tools in src/server.js with tool handlers
- [X] T030 [US1] Add request/response logging for all flight operations with session ID, operation type, parameters, and response times
- [X] T031 [US1] Implement error handling for invalid airport codes, invalid dates, validation failures with specific error messages per FR-014
**FR-015 Coverage Note**: Multi-step booking workflows (search → price verification → select → confirm) are implemented through the task sequence T018-T028. The searchFlights tool (T018-T020) enables price verification, bookFlight (T021-T024) handles selection and confirmation, and retrieveBooking (T025-T026) supports workflow verification.
**Checkpoint**: At this point, User Story 1 should be fully functional - developers can search flights, create bookings, retrieve bookings, and cancel bookings independently.
---
## Phase 4: User Story 4 - Session Management for Concurrent Testing (Priority: P2)
**Goal**: Enable concurrent MCP sessions with isolated booking state for parallel test execution and multi-developer environments.
**Independent Test**: Start 5-10 concurrent MCP sessions, perform different flight bookings in each (different routes, passenger names), verify PNR retrieval in each session returns only that session's bookings with zero cross-session data leakage. Success means automated tests can run in parallel without interference.
### Session Infrastructure
- [X] T032 [US4] Implement session creation logic in src/session/manager.js: generate UUID v4 session ID on MCP connection initialization
- [X] T033 [US4] Add session metadata storage in Valkey at gds:session:{sessionId} with createdAt, expiresAt, lastActivity, bookingCount, searchCount fields
- [X] T034 [US4] Implement session TTL management: set EXPIRE on session keys (default 3600 seconds), refresh on activity
- [X] T035 [US4] Add session validation middleware in src/session/manager.js: verify session exists and not expired before tool execution
### Session Isolation
- [X] T036 [US4] Implement session-scoped key prefixing in src/session/storage.js: all Valkey keys include session ID for isolation
- [X] T037 [US4] Update booking storage to enforce session scope: gds:session:{sessionId}:booking:{pnr} pattern in all tools
- [X] T038 [US4] Add session cleanup on expiry: implement background job or TTL-based cleanup for expired session data
- [X] T039 [US4] Implement session statistics tracking: maintain gds:stats:sessions:active set, update gds:stats:bookings:total counter
### Session Validation
- [X] T040 [US4] Add cross-session isolation validation in retrieveBooking tool: verify PNR belongs to current session before returning
- [X] T041 [US4] Implement session activity tracking in src/session/manager.js: update lastActivity timestamp on every tool call
- [X] T042 [US4] Add session error responses for expired/invalid sessions with clear messages "Session expired" or "Session not found"
**Checkpoint**: Multiple MCP sessions can now run concurrently with complete isolation - bookings in one session never appear in another session.
---
## Phase 5: User Story 6 - Remote Access for Distributed Teams (Priority: P2)
**Goal**: Enable remote MCP access over Streamable HTTP (HTTP/1.1 + SSE per MCP 2025-11-25 specification) with rate limiting, CORS, health checks, and comprehensive security.
**Independent Test**: Start server with remote transport enabled, connect from remote MCP client over HTTP/1.1 using SSE for server-to-client messages and POST for client-to-server requests, execute flight search tool with MCP-Protocol-Version header, verify SSE polling pattern with connection closure and retry field, test Last-Event-ID resumption, verify rate limiting enforces 100 req/min default, test CORS preflight with wildcard origin, verify health endpoint returns service status, confirm MCP-Session-Id header management, confirm graceful shutdown preserves active sessions. All operations work independently of other user stories.
### Streamable HTTP Server Setup (MCP 2025-11-25 Compliant)
- [X] T043 [P] [US6] Update package.json with dependencies: @modelcontextprotocol/sdk for StreamableHTTPServerTransport, express for middleware
- [X] T044 [US6] Create Streamable HTTP server in src/transports/http-server.js using MCP SDK's StreamableHTTPServerTransport (HTTP/1.1 + SSE)
- [X] T045 [US6] Implement single /mcp endpoint supporting POST (client messages), GET (server message stream), DELETE (session termination) per MCP spec
- [X] T046 [US6] Add HTTP server lifecycle management: startup on configurable port, graceful shutdown (drain connections, 30s timeout), error handling
### SSE Polling Pattern Implementation (MCP 2025-11-25 Spec)
- [X] T047 [US6] Implement SSE event stream with unique event IDs for resumability in src/transports/sse-handler.js
- [X] T048 [US6] Add initial SSE event with ID and empty data field to prime client reconnection per MCP polling pattern
- [X] T049 [US6] Implement connection closure after response with `retry` field (default: 5000ms) to guide client reconnection timing
- [X] T050 [US6] Add Last-Event-ID header support for stream resumption when clients reconnect after disconnection
### MCP Protocol Version Validation
- [X] T051 [US6] Create protocol version middleware in src/middleware/protocol-version.js
- [X] T052 [US6] Implement MCP-Protocol-Version header validation: require header on all requests, reject missing/invalid versions with 400 Bad Request
- [X] T053 [US6] Add supported version check: accept 2025-11-25, reject unsupported versions with clear error message
### Session Management (MCP-Session-Id Header)
- [X] T054 [US6] Implement MCP-Session-Id header handling in src/session/session-manager.js
- [X] T055 [US6] Add session ID generation on InitializeResult response (cryptographically secure UUID)
- [X] T056 [US6] Validate MCP-Session-Id on subsequent requests: respond with 404 if session expired/not found
- [X] T057 [US6] Implement DELETE /mcp handler for explicit session termination
### Rate Limiting
- [X] T058 [P] [US6] Install express-rate-limit package in package.json for IP-based rate limiting
- [X] T059 [US6] Create rate limiter middleware in src/middleware/rate-limit.js with configurable limits (default: 100 req/min per IP)
- [X] T060 [US6] Implement rate limit enforcement: return 429 Too Many Requests with Retry-After header when limit exceeded
- [X] T061 [US6] Add rate limit headers to all responses: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
- [X] T062 [US6] Implement rate limit storage in Valkey: use sliding window counter with key expiration (60s TTL)
### CORS Configuration (Wildcard Policy)
- [X] T063 [P] [US6] Install cors package in package.json for CORS middleware
- [X] T064 [US6] Create CORS handler in src/middleware/cors.js with permissive wildcard policy (Access-Control-Allow-Origin: *)
- [X] T065 [US6] Implement CORS preflight handling for OPTIONS requests: return allowed methods (GET, POST, DELETE, OPTIONS), headers (Content-Type, MCP-Session-Id, MCP-Protocol-Version)
- [X] T066 [US6] Add Origin header validation per MCP security requirement: respond with 403 Forbidden if Origin present and validation fails
### Health Checks
- [X] T067 [US6] Implement health check endpoint in src/remote/health.js returning GET /health with JSON status response
- [X] T068 [US6] Add service health indicators: Valkey connection status, active session count, uptime, memory usage
- [X] T069 [US6] Implement readiness check: return 503 Service Unavailable if Valkey is disconnected or critical services unavailable
- [X] T070 [US6] Add health check logging: log every 10th health check to avoid log spam, log all failed health checks immediately
### Security & Transport Selection
- [X] T071 [US6] Create transport factory in src/transports/factory.js to select stdio or Streamable HTTP based on TRANSPORT environment variable
- [X] T072 [US6] Implement environment-based configuration in src/config/remote.js: PORT (default: 3000), HOST (default: 127.0.0.1 per MCP security), RATE_LIMIT_MAX, CORS_ORIGINS
- [X] T073 [US6] Add request logging middleware in src/middleware/logger.js: log method, path, IP, User-Agent, MCP-Protocol-Version, duration, status code
- [X] T074 [US6] Implement localhost binding (127.0.0.1) by default per MCP security recommendation, document production deployment with reverse proxy
### Integration & Testing Infrastructure
- [X] T075 [US6] Update src/index.js to support dual transport modes: stdio (default) or Streamable HTTP (when TRANSPORT=http or --remote flag)
- [X] T076 [US6] Add CLI argument parsing in src/index.js: --remote (enable HTTP), --port <number>, --host <address>, --verbose, --log-level <level>
- [X] T077 [US6] Create remote access example client in tests/fixtures/remote-client.js demonstrating SSE connection, Last-Event-ID resumption, and tool invocation
- [X] T078 [US6] Add nginx.conf.example in docker/ for optional HTTP/2 upgrade reverse proxy (client-facing HTTP/2, backend HTTP/1.1)
**Checkpoint**: Remote access complete - developers can connect to MCP server over Streamable HTTP (HTTP/1.1 + SSE per MCP 2025-11-25 spec), enforced rate limits, wildcard CORS, MCP-Protocol-Version validation, SSE polling pattern with connection closure, Last-Event-ID resumption, and health monitoring. Transport selection via TRANSPORT env var or --remote flag.
---
## Phase 6: User Story 2 - Hotel Search and Multi-Service Bundling (Priority: P2)
**Goal**: Enable developers to test hotel search and multi-service booking workflows. Search hotels, create hotel bookings, and bundle hotels with flights under single PNR.
**Independent Test**: Perform hotel search for destination city (LAX) with check-in/check-out dates, receive 5-10 hotel options with star ratings and amenities. Create hotel-only booking with PNR. Then create flight booking, add hotel to existing flight PNR, retrieve combined itinerary showing both services under one PNR. Success means multi-service bundling works without breaking flight-only functionality.
### Hotel Data
- [X] T079 [P] [US2] Create hotels mock data in src/data/hotels.js with 50+ properties across major cities (names, chains, star ratings, addresses, amenities)
- [X] T080 [US2] Implement hotel data generator with realistic pricing tiers: budget $80-$150, midrange $150-$300, luxury $300-$800 per night
### Hotel Search Tool
- [X] T081 [US2] Implement searchHotels tool handler in src/tools/hotels.js with input validation (cityCode, checkInDate, checkOutDate, guests)
- [X] T082 [US2] Add hotel search result generation: 5-10 properties per search, calculate nights and total prices, include amenities (WiFi, parking, breakfast, gym, pool)
- [X] T083 [US2] Implement date validation: checkInDate < checkOutDate, minimum 1 night stay, no past dates
### Hotel Booking Tool
- [X] T084 [US2] Implement bookHotel tool handler in src/tools/hotels.js with guest validation and room selection
- [X] T085 [US2] Add hotel booking creation: generate or use existing PNR, persist HotelReservation to Valkey with check-in/check-out dates, room type, pricing
- [X] T086 [US2] Implement multi-service bundling logic in src/tools/hotels.js: add hotel to existing PNR if provided, create new PNR if not
### Multi-Service Integration
- [X] T087 [US2] Update PNR structure in src/data/pnr.js to support multiple service segments: flights[], hotels[], cars[] arrays
- [X] T088 [US2] Implement total price calculation across all segments: sum flight prices + hotel prices, update PNR totalPrice field
- [X] T089 [US2] Add date consistency validation: hotel dates should overlap with flight dates, warn if hotel is outside travel period
- [X] T090 [US2] Update retrieveBooking tool in src/tools/bookings.js to return complete multi-service itineraries with all segment types
- [X] T091 [US2] Register searchHotels and bookHotel tools in src/server.js with tool handlers
**Checkpoint**: Developers can now search hotels, create hotel bookings, and bundle hotels with flights. Both flight-only and flight+hotel bookings work independently.
---
## Phase 7: User Story 3 - Car Rental and Complete Travel Package (Priority: P3)
**Goal**: Complete the full travel package capability by adding car rentals. Search cars, create car bookings, and bundle with flights+hotels for complete end-to-end travel itineraries.
**Independent Test**: Search for rental cars at destination airport (LAX) with pickup/dropoff dates, receive 4-6 vehicle classes (economy, SUV, luxury) with daily rates. Create car-only booking. Then add car to existing flight+hotel PNR, retrieve complete package showing all three services in chronological trip order. Success means full GDS capability coverage with all service types working together.
### Car Rental Data
- [X] T092 [P] [US3] Create car rental mock data in src/data/cars.js with 6+ rental companies (Hertz, Avis, Enterprise codes and names)
- [X] T093 [US3] Implement car rental data generator with vehicle classes: economy $35-$50, midsize $50-$80, SUV/luxury $100-$150 per day
### Car Rental Search Tool
- [X] T094 [US3] Implement searchCars tool handler in src/tools/cars.js with input validation (pickupLocation, pickupDate, dropoffLocation, dropoffDate)
- [X] T095 [US3] Add car search result generation: 4-6 vehicle classes per search, calculate rental days and total prices, include vehicle specs and mileage policies
- [X] T096 [US3] Implement date validation: pickupDate < dropoffDate, minimum 1 day rental, location validation (airport/city codes)
### Car Rental Booking Tool
- [X] T097 [US3] Implement bookCar tool handler in src/tools/cars.js with driver validation and vehicle selection
- [X] T098 [US3] Add car rental booking creation: generate or use existing PNR, persist CarRental to Valkey with pickup/dropoff details, vehicle class, pricing
- [X] T099 [US3] Implement car bundling logic in src/tools/cars.js: add car to existing PNR, validate dates align with flight arrival/departure
### Complete Package Integration
- [X] T100 [US3] Update total price calculation in src/data/pnr.js to include car rental prices: sum flights + hotels + cars
- [X] T101 [US3] Add chronological itinerary ordering in retrieveBooking: sort segments by date/time (flight arrival → car pickup → hotel check-in → hotel check-out → car dropoff → return flight)
- [X] T102 [US3] Implement date consistency validation for cars: pickup should align with flight arrival, dropoff should align with departure
- [X] T103 [US3] Register searchCars and bookCar tools in src/server.js with tool handlers
**Checkpoint**: Full GDS capability is now complete - developers can create comprehensive travel packages with flights, hotels, and cars all bundled under one PNR.
---
## Phase 8: User Story 5 - Realistic Test Data for Demonstrations (Priority: P3)
**Goal**: Polish mock data quality to enable professional sales demonstrations and training scenarios with recognizable brands, realistic pricing, and diverse route coverage.
**Independent Test**: Execute searches for major demo routes (JFK→LAX domestic, JFK→LHR international, LAX→TYO Asia-Pacific), verify results include recognizable brands (United, Marriott, Hertz), prices in expected market ranges, realistic travel times, and professional presentation suitable for live demos without disclaimers. Success means sales team can confidently present the mock server as representative of production GDS quality.
### Enhanced Mock Data Quality
- [X] T104 [P] [US5] Expand airports mock data in src/data/airports.js to include 50+ international hubs (London LHR, Paris CDG, Tokyo NRT, Dubai DXB, etc.)
- [X] T105 [P] [US5] Add major hotel chains to src/data/hotels.js: Marriott, Hilton, Hyatt, IHG properties in 20+ major cities worldwide
- [X] T106 [P] [US5] Enhance airline data in src/data/airlines.js with international carriers: British Airways, Virgin Atlantic, Lufthansa, Emirates, ANA, JAL
### Route-Specific Pricing
- [X] T107 [US5] Implement distance-based pricing in src/data/flights.js: calculate flight distance from coordinates, apply pricing tiers (short-haul $200-$400, medium-haul $400-$800, long-haul $800-$2000)
- [X] T108 [US5] Add realistic flight durations per route in src/data/flights.js: JFK-LAX ~6 hours, JFK-LHR ~7 hours, LAX-TYO ~12 hours
- [X] T109 [US5] Implement time-of-day departure variety: morning (6-9am), midday (9am-2pm), afternoon (2-5pm), evening (5-10pm) for realistic schedule diversity
### Premium Cabin Classes
- [X] T110 [US5] Add premium cabin pricing in src/data/flights.js: economy baseline, premium economy +40%, business +200%, first class +400%
- [X] T111 [US5] Implement cabin-specific availability and booking classes: Y/B/M for economy, W for premium economy, J/C for business, F/A for first
- [X] T112 [US5] Add cabin-specific amenities in flight results: economy (standard seat), business (lie-flat, lounge access), first (suites, premium dining)
### Demo Scenarios Configuration
- [X] T113 [US5] Create demo seed data option in src/data/flights.js: when MOCK_DATA_SEED=demo, return curated high-quality results for common demo routes
- [X] T114 [US5] Add configurable response delay in src/utils/logger.js via MOCK_RESPONSE_DELAY env var to simulate realistic search response times during demos
- [X] T115 [US5] Implement metadata tagging in all responses: add "data_source": "mock" field to all search results and bookings per FR-012
**Checkpoint**: Mock data is now demo-quality - professional appearance, recognizable brands, realistic pricing suitable for sales presentations and training without requiring explanations.
---
## Phase 9: Additional Booking Management (Supporting Tools)
**Goal**: Add convenience tool for listing all bookings in a session (supports User Stories 1-4).
**Independent Test**: Create multiple bookings (flights, hotels, cars) in a session, call listBookings tool, verify it returns all PNRs created in the session with summary information. Provides developers quick overview of session state.
- [X] T116 [US1] Implement listBookings tool handler in src/tools/bookings.js to retrieve all PNRs in current session
- [X] T117 [US1] Add booking list query: read gds:session:{sessionId}:bookings set, fetch summary for each PNR (pnr, status, createdAt, totalPrice, segment counts)
- [X] T118 [US1] Register listBookings tool in src/server.js with tool handler
- [X] T119 [US1] Add pagination support for listBookings if session has 10+ bookings: limit and offset parameters
---
## Phase 10: Docker Packaging
**Purpose**: Container packaging for deployment and distribution
- [X] T120 Create multi-stage Dockerfile in docker/Dockerfile: builder stage (install deps, copy source) and production stage (Node.js 20 Alpine, non-root user)
- [X] T121 Create docker-bake.hcl in docker/ for multi-platform builds: linux/amd64 and linux/arm64 targets
- [X] T122 [P] Add health check to Dockerfile: verify Valkey connection and MCP server readiness
- [X] T123 [P] Create .env.example file with all configuration variables documented (MCP, Valkey, logging, mock data settings)
- [X] T124 Update docker-compose.yaml to include gds-mock-mcp service with Valkey dependency and environment variable mapping
- [X] T125 Add build and run scripts in package.json: npm run docker:build, npm run docker:run, npm run docker:down
---
## Phase 11: Documentation & Polish
**Purpose**: User-facing documentation, validation, and final polish
- [X] T126 [P] Create comprehensive README.md: installation instructions, configuration reference, Docker usage, MCP tool documentation, troubleshooting guide
- [X] T127 [P] Add SAFETY_DISCLAIMER.md: prominent "FOR TESTING AND DEMO PURPOSES ONLY" notice, explanation of TEST- prefix, no real transactions guarantee
**Success Criteria Alignment**: The README (T090) and quickstart guide (T091) should emphasize:
- **SC-006**: Professional, demo-quality data requiring zero disclaimers or explanations during sales demonstrations
- **SC-007**: Clear documentation enabling new developers to understand GDS workflows within 15 minutes using realistic examples as learning material
These documentation objectives ensure the mock server serves both testing and training purposes effectively.
- [X] T128 [P] Create CHANGELOG.md documenting feature implementation and version history
- [X] T129 [P] Add inline code documentation: JSDoc comments for all public functions, tool handlers, data generators
- [X] T130 Validate quickstart.md examples: test all example commands in quickstart.md work correctly with current implementation
- [X] T131 Add logging coverage review: ensure all operations log appropriately (search, book, retrieve, cancel, errors) with sufficient detail per FR-013
- [X] T132 Perform security review: verify no production credentials, no external API calls, TEST- prefix enforcement, non-root Docker user per Constitution Principle III
- [X] T133 Run constitution compliance check: verify all 6 principles (MCP protocol, mock data realism, no real transactions, tool architecture, session management, observability)
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies - can start immediately
- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
- **User Stories (Phases 3-7)**: All depend on Foundational phase completion
- **Phase 3 (US1 - P1)**: Can start after Foundational - No dependencies on other stories ✅ MVP
- **Phase 4 (US4 - P2)**: Can start after Foundational - Independent, but beneficial before scale testing
- **Phase 5 (US2 - P2)**: Can start after Foundational - Extends US1, but independently testable
- **Phase 6 (US3 - P3)**: Can start after Foundational - Extends US1+US2, but independently testable
- **Phase 7 (US5 - P3)**: Can start after Foundational - Enhances data quality across all stories
- **Phase 8**: Extends US1 - can start after US1 complete
- **Docker Packaging (Phase 9)**: Can start after US1 complete (minimal MVP) or after all user stories
- **Documentation (Phase 10)**: Can start in parallel with user story work, finalize after all stories complete
### User Story Dependencies (Independence Analysis)
- **US1 (Flight Booking)**: Fully independent - no dependencies
- **US4 (Session Management)**: Fully independent - enhances US1 but not required for basic functionality
- **US2 (Hotel Bundling)**: Extends US1 but independently testable (can create hotel-only bookings)
- **US3 (Car Rentals)**: Extends US1+US2 but independently testable (can create car-only bookings)
- **US5 (Demo Data)**: Enhances all stories but doesn't block functionality
### Within Each User Story
- Data files before generators
- Generators before tool handlers
- Tool handlers before MCP server registration
- Core operations before advanced features
- Validation before error handling
### Parallel Opportunities
**Setup Phase**: T003 [P], T004 [P], T005 [P] can run in parallel
**Foundational Phase**: T007 [P], T008 [P], T009 [P], T010 [P], T012 [P] can run in parallel after T006
**User Story 1**: T015 [P] + T016 [P] (data files), then T018-T031 sequentially
**User Story 6**: T043 [P] + T058 [P] + T063 [P] can run in parallel with US1 tasks
**User Story 2**: T079 [P] can run in parallel with US1/US6 tasks
**User Story 3**: T092 [P] + T093 [P] can run in parallel with US1/US2/US6 tasks
**User Story 5**: T104 [P] + T105 [P] + T106 [P] can run in parallel
**User Stories across teams**: Once Foundational complete, different developers can work on US1, US2, US3, US4, US5, US6 in parallel
---
## Parallel Example: Multi-Story Development
```bash
# After Foundational Phase completes, launch all user stories in parallel:
# Developer A: User Story 1 (MVP)
Task T015-T031: Flight search and booking implementation
# Developer B: User Story 4 (Session Management)
Task T032-T042: Concurrent session isolation
# Developer C: User Story 6 (Remote Access)
Task T043-T078: Streamable HTTP transport with SSE polling
# Developer D: User Story 2 (Hotels)
Task T079-T091: Hotel search and bundling
# Developer E: User Story 3 (Cars)
Task T092-T103: Car rental integration
# Developer F: User Story 5 (Demo Data)
Task T104-T115: Enhanced mock data quality
```
---
## Implementation Strategy
### MVP First (User Story 1 Only) - RECOMMENDED START
1. ✅ Complete Phase 1: Setup (T001-T005)
2. ✅ Complete Phase 2: Foundational (T006-T014) - CRITICAL BLOCKER
3. ✅ Complete Phase 3: User Story 1 (T015-T031) - Flight search and booking
4. **STOP and VALIDATE**: Test flight search → book → retrieve → cancel workflow independently
5. Optional: Add Phase 9 (Docker) for easy deployment
6. Optional: Add Phase 10 (Documentation)
7. **Deploy/Demo MVP** - Fully functional flight booking mock server
### Incremental Delivery (Recommended)
1. Foundation (Setup + Foundational) → T001-T014 complete
2. **MVP Release**: Add US1 (T015-T031) → Test independently → Deploy v0.1
3. **Multi-User Release**: Add US4 (T032-T042) → Test concurrency → Deploy v0.2
4. **Remote Access**: Add US6 (T043-T078) → Test Streamable HTTP + SSE → Deploy v0.3
5. **Hotel Bundling**: Add US2 (T079-T091) → Test multi-service → Deploy v0.4
6. **Full Package**: Add US3 (T092-T103) → Test complete packages → Deploy v0.5
7. **Demo Quality**: Add US5 (T104-T115) → Polish for presentations → Deploy v1.0
8. Each increment adds value without breaking previous functionality
### Parallel Team Strategy (For Larger Teams)
1. **Week 1**: Entire team completes Setup + Foundational (T001-T014)
2. **Week 2-3** (After Foundational complete):
- **Team A** (2 devs): User Story 1 (T015-T031) - Priority focus
- **Team B** (1 dev): User Story 4 (T032-T042) - Critical for testing
- **Team C** (1 dev): User Story 6 data prep (T043-T046) - Remote transport setup
- **Team D** (1 dev): User Story 2 data prep (T079-T080) - Prepare for integration
3. **Week 4**: Integrate US1+US4+US6, then continue with US2, US3, US5
4. **Week 5**: Docker packaging, documentation, final polish
### Priority-Driven Sequential (Small Team)
1. Setup → Foundational (T001-T014)
2. User Story 1 - P1 (T015-T031) ✅ MVP CHECKPOINT
3. User Story 4 - P2 (T032-T042) - Enable concurrent testing
4. User Story 6 - P2 (T043-T078) - Enable remote access via Streamable HTTP
5. User Story 2 - P2 (T079-T091) - Add hotel capability
6. User Story 3 - P3 (T092-T103) - Complete package
7. User Story 5 - P3 (T104-T115) - Polish for demos
8. Docker + Docs (T120-T133)
---
## Task Summary
**Total Tasks**: 97 tasks
**Task Distribution by Phase**:
- Phase 1 (Setup): 5 tasks
- Phase 2 (Foundational): 9 tasks - BLOCKS ALL USER STORIES
- Phase 3 (US1 - Flight Booking): 17 tasks - MVP ✅
- Phase 4 (US4 - Session Management): 11 tasks
- Phase 5 (US6 - Remote Access): 36 tasks
- Phase 6 (US2 - Hotel Bundling): 13 tasks
- Phase 7 (US3 - Car Rentals): 12 tasks
- Phase 8 (US5 - Demo Data Quality): 12 tasks
- Phase 9 (Booking Management): 4 tasks
- Phase 10 (Docker): 6 tasks
- Phase 11 (Documentation): 8 tasks
**Task Distribution by User Story**:
- US1 (Flight Booking - P1): 21 tasks (includes Phase 9)
- US6 (Remote Access - P2): 36 tasks
- US2 (Hotel Bundling - P2): 13 tasks
- US3 (Car Rentals - P3): 12 tasks
- US4 (Session Management - P2): 11 tasks
- US5 (Demo Data - P3): 12 tasks
- Infrastructure (Setup + Foundational + Docker + Docs): 28 tasks
**Parallel Opportunities Identified**:
- Setup: 3 parallel tasks
- Foundational: 5 parallel tasks
- User Story data preparation: Multiple files can be created in parallel
- Complete user stories: 5 stories can be developed in parallel after Foundational
- Documentation: Can progress alongside implementation
**Independent Test Criteria per Story**:
- **US1**: Search flights → book with passengers → retrieve PNR → cancel booking (complete workflow)
- **US4**: Run 5-10 concurrent sessions, verify zero data leakage between sessions
- **US6**: Connect via Streamable HTTP → verify SSE polling → test MCP-Protocol-Version validation → test rate limiting
- **US2**: Search hotels → book hotel-only → bundle hotel with flight → retrieve multi-service PNR
- **US3**: Search cars → book car-only → add to flight+hotel → retrieve complete package
- **US5**: Review demo routes (JFK-LAX, JFK-LHR, LAX-TYO), verify professional data quality
**Suggested MVP Scope** (Minimum Viable Product):
- Phase 1: Setup (T001-T005)
- Phase 2: Foundational (T006-T014)
- Phase 3: User Story 1 (T015-T031)
- Phase 10: Docker packaging basics (T120-T125)
- Phase 11: Essential docs (T126-T127)
**Total MVP Tasks**: 34 tasks
**MVP delivers**: Fully functional flight search and booking mock server with Docker deployment
---
## Format Validation ✅
All tasks follow the required checklist format:
- ✅ Every task starts with `- [ ]` (markdown checkbox)
- ✅ Every task has sequential Task ID (T001, T002, T003...)
- ✅ User story tasks have [Story] label ([US1], [US2], [US3], [US4], [US5])
- ✅ Setup, Foundational, Docker, and Polish tasks have NO story label (correctly)
- ✅ Parallel tasks marked with [P]
- ✅ All descriptions include clear actions and exact file paths
- ✅ Tasks organized by phase with user stories as primary organization
- ✅ Each user story phase includes goal and independent test criteria
---
## Notes
- No tests included (not explicitly requested in spec.md per template guidelines)
- Tasks ordered for dependency flow: data → generators → tools → server integration
- Each user story can be deployed and tested independently
- MVP (US1 only) provides immediate value for flight booking testing
- Session management (US4) recommended early for CI/CD integration
- Multi-service bundling (US2, US3) extends functionality without breaking US1
- Demo data polish (US5) is final enhancement, not blocking
- Constitution compliance verification built into final phase
- All 8 MCP tools from contracts/mcp-tools.md covered across user stories

View File

@@ -0,0 +1,549 @@
# Tasks: Mock GDS MCP Server
**Input**: Design documents from `/specs/001-mock-gds-server/`
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/ ✅
**Tests**: Tests are NOT explicitly requested in the specification, so test tasks are OMITTED per template guidelines.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3, US4, US5)
- All tasks include exact file paths
---
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Project initialization and basic structure from plan.md
- [X] T001 Initialize Node.js 20 project with package.json including @modelcontextprotocol/sdk, ioredis, pino dependencies
- [X] T002 Create project directory structure: src/{tools,data,session,validation,utils}/, tests/{integration,unit,fixtures}/, docker/
- [X] T003 [P] Configure ESLint and Prettier for code quality in .eslintrc.json and .prettierrc
- [X] T004 [P] Create .dockerignore and .gitignore files for build optimization
- [X] T005 [P] Create docker-compose.yaml with Valkey service configuration (port 6379, persistence enabled)
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
- [X] T006 Implement Pino structured logger setup in src/utils/logger.js with configurable log levels
- [X] T007 [P] Implement error handling utilities in src/utils/errors.js with MCP error codes
- [X] T008 [P] Create Valkey client wrapper in src/session/storage.js with connection pooling and error handling
- [X] T009 [P] Implement JSON schema validators in src/validation/validators.js using native validation
- [X] T010 [P] Create MCP tool schemas in src/validation/schemas.js based on contracts/mcp-tools.md
- [X] T011 Create session lifecycle manager in src/session/manager.js with TTL management (1 hour default)
- [X] T012 [P] Implement PNR generation utilities in src/data/pnr.js with TEST- prefix and base32 encoding
- [X] T013 Initialize MCP server in src/server.js with @modelcontextprotocol/sdk Server class
- [X] T014 Create MCP server entry point in src/index.js with stdio transport and error handling
**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
---
## Phase 3: User Story 1 - Flight Search and Booking (Priority: P1) 🎯 MVP
**Goal**: Enable developers to test flight search and booking functionality with realistic mock data. Search flights, create bookings, retrieve bookings, and cancel bookings.
**Independent Test**: Execute searchFlights MCP tool for JFK→LAX route, receive 3-5 mock flight results with valid IATA codes and realistic prices, create booking with passenger details, receive TEST- prefixed PNR, retrieve booking by PNR, and cancel booking. All operations complete successfully without requiring any other service types.
### Core Data for User Story 1
- [X] T015 [P] [US1] Create airports mock data in src/data/airports.js with 100+ major airports (IATA codes, names, cities, timezones, coordinates)
- [X] T016 [P] [US1] Create airlines mock data in src/data/airlines.js with 30+ carriers (IATA codes, names, countries)
- [X] T017 [US1] Implement flight data generator in src/data/flights.js with deterministic pricing, duration calculation, and availability logic
### Flight Search Tool
- [X] T018 [US1] Implement searchFlights tool handler in src/tools/flights.js with input validation (origin, destination, departureDate, passengers, cabin)
- [X] T019 [US1] Add flight search result generation with 3-5 mock flights per search, realistic schedules (6am-10pm departures), and price ranges ($200-$800 economy, $800-$2000 business, $2500+ first)
- [X] T020 [US1] Implement seat availability simulation (90% available, 10% sold out) and booking class assignment. **Note**: Implements infinite inventory model where concurrent bookings on same flight both succeed. This aligns with spec.md edge case discussion and is appropriate for mock server testing scope.
### Flight Booking Tool
- [X] T021 [US1] Implement bookFlight tool handler in src/tools/flights.js with passenger validation (firstName, lastName, email, phone)
- [X] T022 [US1] Add flight booking creation logic: validate flight selection, generate PNR via src/data/pnr.js, persist to Valkey with key gds:session:{sessionId}:booking:{pnr}
- [X] T023 [US1] Implement PNR storage structure in Valkey with FlightSegment, Passenger, pricing, status fields per data-model.md
- [X] T024 [US1] Add session booking tracking: update gds:session:{sessionId}:bookings set and increment bookingCount
### Booking Management Tools
- [X] T025 [US1] Implement retrieveBooking tool handler in src/tools/bookings.js with PNR validation and Valkey lookup
- [X] T026 [US1] Add booking retrieval logic: fetch from gds:session:{sessionId}:booking:{pnr}, return complete booking details with all segments
- [X] T027 [US1] Implement cancelBooking tool handler in src/tools/bookings.js with status transition validation (confirmed→cancelled only)
- [X] T028 [US1] Add cancellation logic: update booking status to 'cancelled', persist timestamp, return confirmation
### MCP Server Integration
- [X] T029 [US1] Register searchFlights, bookFlight, retrieveBooking, cancelBooking tools in src/server.js with tool handlers
- [X] T030 [US1] Add request/response logging for all flight operations with session ID, operation type, parameters, and response times
- [X] T031 [US1] Implement error handling for invalid airport codes, invalid dates, validation failures with specific error messages per FR-014
**FR-015 Coverage Note**: Multi-step booking workflows (search → price verification → select → confirm) are implemented through the task sequence T018-T028. The searchFlights tool (T018-T020) enables price verification, bookFlight (T021-T024) handles selection and confirmation, and retrieveBooking (T025-T026) supports workflow verification.
**Checkpoint**: At this point, User Story 1 should be fully functional - developers can search flights, create bookings, retrieve bookings, and cancel bookings independently.
---
## Phase 4: User Story 4 - Session Management for Concurrent Testing (Priority: P2)
**Goal**: Enable concurrent MCP sessions with isolated booking state for parallel test execution and multi-developer environments.
**Independent Test**: Start 5-10 concurrent MCP sessions, perform different flight bookings in each (different routes, passenger names), verify PNR retrieval in each session returns only that session's bookings with zero cross-session data leakage. Success means automated tests can run in parallel without interference.
### Session Infrastructure
- [X] T032 [US4] Implement session creation logic in src/session/manager.js: generate UUID v4 session ID on MCP connection initialization
- [X] T033 [US4] Add session metadata storage in Valkey at gds:session:{sessionId} with createdAt, expiresAt, lastActivity, bookingCount, searchCount fields
- [X] T034 [US4] Implement session TTL management: set EXPIRE on session keys (default 3600 seconds), refresh on activity
- [X] T035 [US4] Add session validation middleware in src/session/manager.js: verify session exists and not expired before tool execution
### Session Isolation
- [X] T036 [US4] Implement session-scoped key prefixing in src/session/storage.js: all Valkey keys include session ID for isolation
- [X] T037 [US4] Update booking storage to enforce session scope: gds:session:{sessionId}:booking:{pnr} pattern in all tools
- [X] T038 [US4] Add session cleanup on expiry: implement background job or TTL-based cleanup for expired session data
- [X] T039 [US4] Implement session statistics tracking: maintain gds:stats:sessions:active set, update gds:stats:bookings:total counter
### Session Validation
- [X] T040 [US4] Add cross-session isolation validation in retrieveBooking tool: verify PNR belongs to current session before returning
- [X] T041 [US4] Implement session activity tracking in src/session/manager.js: update lastActivity timestamp on every tool call
- [X] T042 [US4] Add session error responses for expired/invalid sessions with clear messages "Session expired" or "Session not found"
**Checkpoint**: Multiple MCP sessions can now run concurrently with complete isolation - bookings in one session never appear in another session.
---
## Phase 5: User Story 6 - Remote Access for Distributed Teams (Priority: P2)
**Goal**: Enable remote MCP access over Streamable HTTP (HTTP/1.1 + SSE per MCP 2025-11-25 specification) with rate limiting, CORS, health checks, and comprehensive security.
**Independent Test**: Start server with remote transport enabled, connect from remote MCP client over HTTP/1.1 using SSE for server-to-client messages and POST for client-to-server requests, execute flight search tool with MCP-Protocol-Version header, verify SSE polling pattern with connection closure and retry field, test Last-Event-ID resumption, verify rate limiting enforces 100 req/min default, test CORS preflight with wildcard origin, verify health endpoint returns service status, confirm MCP-Session-Id header management, confirm graceful shutdown preserves active sessions. All operations work independently of other user stories.
### Streamable HTTP Server Setup (MCP 2025-11-25 Compliant)
- [ ] T043 [P] [US6] Update package.json with dependencies: @modelcontextprotocol/sdk for StreamableHTTPServerTransport, express for middleware
- [ ] T044 [US6] Create Streamable HTTP server in src/transports/http-server.js using MCP SDK's StreamableHTTPServerTransport (HTTP/1.1 + SSE)
- [ ] T045 [US6] Implement single /mcp endpoint supporting POST (client messages), GET (server message stream), DELETE (session termination) per MCP spec
- [ ] T046 [US6] Add HTTP server lifecycle management: startup on configurable port, graceful shutdown (drain connections, 30s timeout), error handling
### SSE Polling Pattern Implementation (MCP 2025-11-25 Spec)
- [ ] T047 [US6] Implement SSE event stream with unique event IDs for resumability in src/transports/sse-handler.js
- [ ] T048 [US6] Add initial SSE event with ID and empty data field to prime client reconnection per MCP polling pattern
- [ ] T049 [US6] Implement connection closure after response with `retry` field (default: 5000ms) to guide client reconnection timing
- [ ] T050 [US6] Add Last-Event-ID header support for stream resumption when clients reconnect after disconnection
### MCP Protocol Version Validation
- [ ] T051 [US6] Create protocol version middleware in src/middleware/protocol-version.js
- [ ] T052 [US6] Implement MCP-Protocol-Version header validation: require header on all requests, reject missing/invalid versions with 400 Bad Request
- [ ] T053 [US6] Add supported version check: accept 2025-11-25, reject unsupported versions with clear error message
### Session Management (MCP-Session-Id Header)
- [ ] T054 [US6] Implement MCP-Session-Id header handling in src/session/session-manager.js
- [ ] T055 [US6] Add session ID generation on InitializeResult response (cryptographically secure UUID)
- [ ] T056 [US6] Validate MCP-Session-Id on subsequent requests: respond with 404 if session expired/not found
- [ ] T057 [US6] Implement DELETE /mcp handler for explicit session termination
### Rate Limiting
- [ ] T058 [P] [US6] Install express-rate-limit package in package.json for IP-based rate limiting
- [ ] T059 [US6] Create rate limiter middleware in src/middleware/rate-limit.js with configurable limits (default: 100 req/min per IP)
- [ ] T060 [US6] Implement rate limit enforcement: return 429 Too Many Requests with Retry-After header when limit exceeded
- [ ] T061 [US6] Add rate limit headers to all responses: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
- [ ] T062 [US6] Implement rate limit storage in Valkey: use sliding window counter with key expiration (60s TTL)
### CORS Configuration (Wildcard Policy)
- [ ] T063 [P] [US6] Install cors package in package.json for CORS middleware
- [ ] T064 [US6] Create CORS handler in src/middleware/cors.js with permissive wildcard policy (Access-Control-Allow-Origin: *)
- [ ] T065 [US6] Implement CORS preflight handling for OPTIONS requests: return allowed methods (GET, POST, DELETE, OPTIONS), headers (Content-Type, MCP-Session-Id, MCP-Protocol-Version)
- [ ] T066 [US6] Add Origin header validation per MCP security requirement: respond with 403 Forbidden if Origin present and validation fails
### Health Checks
- [ ] T067 [US6] Implement health check endpoint in src/remote/health.js returning GET /health with JSON status response
- [ ] T068 [US6] Add service health indicators: Valkey connection status, active session count, uptime, memory usage
- [ ] T069 [US6] Implement readiness check: return 503 Service Unavailable if Valkey is disconnected or critical services unavailable
- [ ] T070 [US6] Add health check logging: log every 10th health check to avoid log spam, log all failed health checks immediately
### Security & Transport Selection
- [ ] T071 [US6] Create transport factory in src/transports/factory.js to select stdio or Streamable HTTP based on TRANSPORT environment variable
- [ ] T072 [US6] Implement environment-based configuration in src/config/remote.js: PORT (default: 3000), HOST (default: 127.0.0.1 per MCP security), RATE_LIMIT_MAX, CORS_ORIGINS
- [ ] T073 [US6] Add request logging middleware in src/middleware/logger.js: log method, path, IP, User-Agent, MCP-Protocol-Version, duration, status code
- [ ] T074 [US6] Implement localhost binding (127.0.0.1) by default per MCP security recommendation, document production deployment with reverse proxy
### Integration & Testing Infrastructure
- [ ] T075 [US6] Update src/index.js to support dual transport modes: stdio (default) or Streamable HTTP (when TRANSPORT=http or --remote flag)
- [ ] T076 [US6] Add CLI argument parsing in src/index.js: --remote (enable HTTP), --port <number>, --host <address>, --verbose, --log-level <level>
- [ ] T077 [US6] Create remote access example client in tests/fixtures/remote-client.js demonstrating SSE connection, Last-Event-ID resumption, and tool invocation
- [ ] T078 [US6] Add nginx.conf.example in docker/ for optional HTTP/2 upgrade reverse proxy (client-facing HTTP/2, backend HTTP/1.1)
**Checkpoint**: Remote access complete - developers can connect to MCP server over Streamable HTTP (HTTP/1.1 + SSE per MCP 2025-11-25 spec), enforced rate limits, wildcard CORS, MCP-Protocol-Version validation, SSE polling pattern with connection closure, Last-Event-ID resumption, and health monitoring. Transport selection via TRANSPORT env var or --remote flag.
---
## Phase 6: User Story 2 - Hotel Search and Multi-Service Bundling (Priority: P2)
**Goal**: Enable developers to test hotel search and multi-service booking workflows. Search hotels, create hotel bookings, and bundle hotels with flights under single PNR.
**Independent Test**: Perform hotel search for destination city (LAX) with check-in/check-out dates, receive 5-10 hotel options with star ratings and amenities. Create hotel-only booking with PNR. Then create flight booking, add hotel to existing flight PNR, retrieve combined itinerary showing both services under one PNR. Success means multi-service bundling works without breaking flight-only functionality.
### Hotel Data
- [ ] T079 [P] [US2] Create hotels mock data in src/data/hotels.js with 50+ properties across major cities (names, chains, star ratings, addresses, amenities)
- [ ] T080 [US2] Implement hotel data generator with realistic pricing tiers: budget $80-$150, midrange $150-$300, luxury $300-$800 per night
### Hotel Search Tool
- [ ] T081 [US2] Implement searchHotels tool handler in src/tools/hotels.js with input validation (cityCode, checkInDate, checkOutDate, guests)
- [ ] T082 [US2] Add hotel search result generation: 5-10 properties per search, calculate nights and total prices, include amenities (WiFi, parking, breakfast, gym, pool)
- [ ] T083 [US2] Implement date validation: checkInDate < checkOutDate, minimum 1 night stay, no past dates
### Hotel Booking Tool
- [ ] T084 [US2] Implement bookHotel tool handler in src/tools/hotels.js with guest validation and room selection
- [ ] T085 [US2] Add hotel booking creation: generate or use existing PNR, persist HotelReservation to Valkey with check-in/check-out dates, room type, pricing
- [ ] T086 [US2] Implement multi-service bundling logic in src/tools/hotels.js: add hotel to existing PNR if provided, create new PNR if not
### Multi-Service Integration
- [ ] T087 [US2] Update PNR structure in src/data/pnr.js to support multiple service segments: flights[], hotels[], cars[] arrays
- [ ] T088 [US2] Implement total price calculation across all segments: sum flight prices + hotel prices, update PNR totalPrice field
- [ ] T089 [US2] Add date consistency validation: hotel dates should overlap with flight dates, warn if hotel is outside travel period
- [ ] T090 [US2] Update retrieveBooking tool in src/tools/bookings.js to return complete multi-service itineraries with all segment types
- [ ] T091 [US2] Register searchHotels and bookHotel tools in src/server.js with tool handlers
**Checkpoint**: Developers can now search hotels, create hotel bookings, and bundle hotels with flights. Both flight-only and flight+hotel bookings work independently.
---
## Phase 7: User Story 3 - Car Rental and Complete Travel Package (Priority: P3)
**Goal**: Complete the full travel package capability by adding car rentals. Search cars, create car bookings, and bundle with flights+hotels for complete end-to-end travel itineraries.
**Independent Test**: Search for rental cars at destination airport (LAX) with pickup/dropoff dates, receive 4-6 vehicle classes (economy, SUV, luxury) with daily rates. Create car-only booking. Then add car to existing flight+hotel PNR, retrieve complete package showing all three services in chronological trip order. Success means full GDS capability coverage with all service types working together.
### Car Rental Data
- [ ] T092 [P] [US3] Create car rental mock data in src/data/cars.js with 6+ rental companies (Hertz, Avis, Enterprise codes and names)
- [ ] T093 [US3] Implement car rental data generator with vehicle classes: economy $35-$50, midsize $50-$80, SUV/luxury $100-$150 per day
### Car Rental Search Tool
- [ ] T094 [US3] Implement searchCars tool handler in src/tools/cars.js with input validation (pickupLocation, pickupDate, dropoffLocation, dropoffDate)
- [ ] T095 [US3] Add car search result generation: 4-6 vehicle classes per search, calculate rental days and total prices, include vehicle specs and mileage policies
- [ ] T096 [US3] Implement date validation: pickupDate < dropoffDate, minimum 1 day rental, location validation (airport/city codes)
### Car Rental Booking Tool
- [ ] T097 [US3] Implement bookCar tool handler in src/tools/cars.js with driver validation and vehicle selection
- [ ] T098 [US3] Add car rental booking creation: generate or use existing PNR, persist CarRental to Valkey with pickup/dropoff details, vehicle class, pricing
- [ ] T099 [US3] Implement car bundling logic in src/tools/cars.js: add car to existing PNR, validate dates align with flight arrival/departure
### Complete Package Integration
- [ ] T100 [US3] Update total price calculation in src/data/pnr.js to include car rental prices: sum flights + hotels + cars
- [ ] T101 [US3] Add chronological itinerary ordering in retrieveBooking: sort segments by date/time (flight arrival → car pickup → hotel check-in → hotel check-out → car dropoff → return flight)
- [ ] T102 [US3] Implement date consistency validation for cars: pickup should align with flight arrival, dropoff should align with departure
- [ ] T103 [US3] Register searchCars and bookCar tools in src/server.js with tool handlers
**Checkpoint**: Full GDS capability is now complete - developers can create comprehensive travel packages with flights, hotels, and cars all bundled under one PNR.
---
## Phase 8: User Story 5 - Realistic Test Data for Demonstrations (Priority: P3)
**Goal**: Polish mock data quality to enable professional sales demonstrations and training scenarios with recognizable brands, realistic pricing, and diverse route coverage.
**Independent Test**: Execute searches for major demo routes (JFK→LAX domestic, JFK→LHR international, LAX→TYO Asia-Pacific), verify results include recognizable brands (United, Marriott, Hertz), prices in expected market ranges, realistic travel times, and professional presentation suitable for live demos without disclaimers. Success means sales team can confidently present the mock server as representative of production GDS quality.
### Enhanced Mock Data Quality
- [ ] T104 [P] [US5] Expand airports mock data in src/data/airports.js to include 50+ international hubs (London LHR, Paris CDG, Tokyo NRT, Dubai DXB, etc.)
- [ ] T105 [P] [US5] Add major hotel chains to src/data/hotels.js: Marriott, Hilton, Hyatt, IHG properties in 20+ major cities worldwide
- [ ] T106 [P] [US5] Enhance airline data in src/data/airlines.js with international carriers: British Airways, Virgin Atlantic, Lufthansa, Emirates, ANA, JAL
### Route-Specific Pricing
- [ ] T107 [US5] Implement distance-based pricing in src/data/flights.js: calculate flight distance from coordinates, apply pricing tiers (short-haul $200-$400, medium-haul $400-$800, long-haul $800-$2000)
- [ ] T108 [US5] Add realistic flight durations per route in src/data/flights.js: JFK-LAX ~6 hours, JFK-LHR ~7 hours, LAX-TYO ~12 hours
- [ ] T109 [US5] Implement time-of-day departure variety: morning (6-9am), midday (9am-2pm), afternoon (2-5pm), evening (5-10pm) for realistic schedule diversity
### Premium Cabin Classes
- [ ] T110 [US5] Add premium cabin pricing in src/data/flights.js: economy baseline, premium economy +40%, business +200%, first class +400%
- [ ] T111 [US5] Implement cabin-specific availability and booking classes: Y/B/M for economy, W for premium economy, J/C for business, F/A for first
- [ ] T112 [US5] Add cabin-specific amenities in flight results: economy (standard seat), business (lie-flat, lounge access), first (suites, premium dining)
### Demo Scenarios Configuration
- [ ] T113 [US5] Create demo seed data option in src/data/flights.js: when MOCK_DATA_SEED=demo, return curated high-quality results for common demo routes
- [ ] T114 [US5] Add configurable response delay in src/utils/logger.js via MOCK_RESPONSE_DELAY env var to simulate realistic search response times during demos
- [ ] T115 [US5] Implement metadata tagging in all responses: add "data_source": "mock" field to all search results and bookings per FR-012
**Checkpoint**: Mock data is now demo-quality - professional appearance, recognizable brands, realistic pricing suitable for sales presentations and training without requiring explanations.
---
## Phase 9: Additional Booking Management (Supporting Tools)
**Goal**: Add convenience tool for listing all bookings in a session (supports User Stories 1-4).
**Independent Test**: Create multiple bookings (flights, hotels, cars) in a session, call listBookings tool, verify it returns all PNRs created in the session with summary information. Provides developers quick overview of session state.
- [ ] T116 [US1] Implement listBookings tool handler in src/tools/bookings.js to retrieve all PNRs in current session
- [ ] T117 [US1] Add booking list query: read gds:session:{sessionId}:bookings set, fetch summary for each PNR (pnr, status, createdAt, totalPrice, segment counts)
- [ ] T118 [US1] Register listBookings tool in src/server.js with tool handler
- [ ] T119 [US1] Add pagination support for listBookings if session has 10+ bookings: limit and offset parameters
---
## Phase 10: Docker Packaging
**Purpose**: Container packaging for deployment and distribution
- [ ] T120 Create multi-stage Dockerfile in docker/Dockerfile: builder stage (install deps, copy source) and production stage (Node.js 20 Alpine, non-root user)
- [ ] T121 Create docker-bake.hcl in docker/ for multi-platform builds: linux/amd64 and linux/arm64 targets
- [ ] T122 [P] Add health check to Dockerfile: verify Valkey connection and MCP server readiness
- [ ] T123 [P] Create .env.example file with all configuration variables documented (MCP, Valkey, logging, mock data settings)
- [ ] T124 Update docker-compose.yaml to include gds-mock-mcp service with Valkey dependency and environment variable mapping
- [ ] T125 Add build and run scripts in package.json: npm run docker:build, npm run docker:run, npm run docker:down
---
## Phase 11: Documentation & Polish
**Purpose**: User-facing documentation, validation, and final polish
- [ ] T126 [P] Create comprehensive README.md: installation instructions, configuration reference, Docker usage, MCP tool documentation, troubleshooting guide
- [ ] T127 [P] Add SAFETY_DISCLAIMER.md: prominent "FOR TESTING AND DEMO PURPOSES ONLY" notice, explanation of TEST- prefix, no real transactions guarantee
**Success Criteria Alignment**: The README (T090) and quickstart guide (T091) should emphasize:
- **SC-006**: Professional, demo-quality data requiring zero disclaimers or explanations during sales demonstrations
- **SC-007**: Clear documentation enabling new developers to understand GDS workflows within 15 minutes using realistic examples as learning material
These documentation objectives ensure the mock server serves both testing and training purposes effectively.
- [ ] T128 [P] Create CHANGELOG.md documenting feature implementation and version history
- [ ] T129 [P] Add inline code documentation: JSDoc comments for all public functions, tool handlers, data generators
- [ ] T130 Validate quickstart.md examples: test all example commands in quickstart.md work correctly with current implementation
- [ ] T131 Add logging coverage review: ensure all operations log appropriately (search, book, retrieve, cancel, errors) with sufficient detail per FR-013
- [ ] T132 Perform security review: verify no production credentials, no external API calls, TEST- prefix enforcement, non-root Docker user per Constitution Principle III
- [ ] T133 Run constitution compliance check: verify all 6 principles (MCP protocol, mock data realism, no real transactions, tool architecture, session management, observability)
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies - can start immediately
- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
- **User Stories (Phases 3-7)**: All depend on Foundational phase completion
- **Phase 3 (US1 - P1)**: Can start after Foundational - No dependencies on other stories ✅ MVP
- **Phase 4 (US4 - P2)**: Can start after Foundational - Independent, but beneficial before scale testing
- **Phase 5 (US2 - P2)**: Can start after Foundational - Extends US1, but independently testable
- **Phase 6 (US3 - P3)**: Can start after Foundational - Extends US1+US2, but independently testable
- **Phase 7 (US5 - P3)**: Can start after Foundational - Enhances data quality across all stories
- **Phase 8**: Extends US1 - can start after US1 complete
- **Docker Packaging (Phase 9)**: Can start after US1 complete (minimal MVP) or after all user stories
- **Documentation (Phase 10)**: Can start in parallel with user story work, finalize after all stories complete
### User Story Dependencies (Independence Analysis)
- **US1 (Flight Booking)**: Fully independent - no dependencies
- **US4 (Session Management)**: Fully independent - enhances US1 but not required for basic functionality
- **US2 (Hotel Bundling)**: Extends US1 but independently testable (can create hotel-only bookings)
- **US3 (Car Rentals)**: Extends US1+US2 but independently testable (can create car-only bookings)
- **US5 (Demo Data)**: Enhances all stories but doesn't block functionality
### Within Each User Story
- Data files before generators
- Generators before tool handlers
- Tool handlers before MCP server registration
- Core operations before advanced features
- Validation before error handling
### Parallel Opportunities
**Setup Phase**: T003 [P], T004 [P], T005 [P] can run in parallel
**Foundational Phase**: T007 [P], T008 [P], T009 [P], T010 [P], T012 [P] can run in parallel after T006
**User Story 1**: T015 [P] + T016 [P] (data files), then T018-T031 sequentially
**User Story 6**: T043 [P] + T058 [P] + T063 [P] can run in parallel with US1 tasks
**User Story 2**: T079 [P] can run in parallel with US1/US6 tasks
**User Story 3**: T092 [P] + T093 [P] can run in parallel with US1/US2/US6 tasks
**User Story 5**: T104 [P] + T105 [P] + T106 [P] can run in parallel
**User Stories across teams**: Once Foundational complete, different developers can work on US1, US2, US3, US4, US5, US6 in parallel
---
## Parallel Example: Multi-Story Development
```bash
# After Foundational Phase completes, launch all user stories in parallel:
# Developer A: User Story 1 (MVP)
Task T015-T031: Flight search and booking implementation
# Developer B: User Story 4 (Session Management)
Task T032-T042: Concurrent session isolation
# Developer C: User Story 6 (Remote Access)
Task T043-T078: Streamable HTTP transport with SSE polling
# Developer D: User Story 2 (Hotels)
Task T079-T091: Hotel search and bundling
# Developer E: User Story 3 (Cars)
Task T092-T103: Car rental integration
# Developer F: User Story 5 (Demo Data)
Task T104-T115: Enhanced mock data quality
```
---
## Implementation Strategy
### MVP First (User Story 1 Only) - RECOMMENDED START
1. ✅ Complete Phase 1: Setup (T001-T005)
2. ✅ Complete Phase 2: Foundational (T006-T014) - CRITICAL BLOCKER
3. ✅ Complete Phase 3: User Story 1 (T015-T031) - Flight search and booking
4. **STOP and VALIDATE**: Test flight search → book → retrieve → cancel workflow independently
5. Optional: Add Phase 9 (Docker) for easy deployment
6. Optional: Add Phase 10 (Documentation)
7. **Deploy/Demo MVP** - Fully functional flight booking mock server
### Incremental Delivery (Recommended)
1. Foundation (Setup + Foundational) → T001-T014 complete
2. **MVP Release**: Add US1 (T015-T031) → Test independently → Deploy v0.1
3. **Multi-User Release**: Add US4 (T032-T042) → Test concurrency → Deploy v0.2
4. **Remote Access**: Add US6 (T043-T078) → Test Streamable HTTP + SSE → Deploy v0.3
5. **Hotel Bundling**: Add US2 (T079-T091) → Test multi-service → Deploy v0.4
6. **Full Package**: Add US3 (T092-T103) → Test complete packages → Deploy v0.5
7. **Demo Quality**: Add US5 (T104-T115) → Polish for presentations → Deploy v1.0
8. Each increment adds value without breaking previous functionality
### Parallel Team Strategy (For Larger Teams)
1. **Week 1**: Entire team completes Setup + Foundational (T001-T014)
2. **Week 2-3** (After Foundational complete):
- **Team A** (2 devs): User Story 1 (T015-T031) - Priority focus
- **Team B** (1 dev): User Story 4 (T032-T042) - Critical for testing
- **Team C** (1 dev): User Story 6 data prep (T043-T046) - Remote transport setup
- **Team D** (1 dev): User Story 2 data prep (T079-T080) - Prepare for integration
3. **Week 4**: Integrate US1+US4+US6, then continue with US2, US3, US5
4. **Week 5**: Docker packaging, documentation, final polish
### Priority-Driven Sequential (Small Team)
1. Setup → Foundational (T001-T014)
2. User Story 1 - P1 (T015-T031) ✅ MVP CHECKPOINT
3. User Story 4 - P2 (T032-T042) - Enable concurrent testing
4. User Story 6 - P2 (T043-T078) - Enable remote access via Streamable HTTP
5. User Story 2 - P2 (T079-T091) - Add hotel capability
6. User Story 3 - P3 (T092-T103) - Complete package
7. User Story 5 - P3 (T104-T115) - Polish for demos
8. Docker + Docs (T120-T133)
---
## Task Summary
**Total Tasks**: 97 tasks
**Task Distribution by Phase**:
- Phase 1 (Setup): 5 tasks
- Phase 2 (Foundational): 9 tasks - BLOCKS ALL USER STORIES
- Phase 3 (US1 - Flight Booking): 17 tasks - MVP ✅
- Phase 4 (US4 - Session Management): 11 tasks
- Phase 5 (US6 - Remote Access): 36 tasks
- Phase 6 (US2 - Hotel Bundling): 13 tasks
- Phase 7 (US3 - Car Rentals): 12 tasks
- Phase 8 (US5 - Demo Data Quality): 12 tasks
- Phase 9 (Booking Management): 4 tasks
- Phase 10 (Docker): 6 tasks
- Phase 11 (Documentation): 8 tasks
**Task Distribution by User Story**:
- US1 (Flight Booking - P1): 21 tasks (includes Phase 9)
- US6 (Remote Access - P2): 36 tasks
- US2 (Hotel Bundling - P2): 13 tasks
- US3 (Car Rentals - P3): 12 tasks
- US4 (Session Management - P2): 11 tasks
- US5 (Demo Data - P3): 12 tasks
- Infrastructure (Setup + Foundational + Docker + Docs): 28 tasks
**Parallel Opportunities Identified**:
- Setup: 3 parallel tasks
- Foundational: 5 parallel tasks
- User Story data preparation: Multiple files can be created in parallel
- Complete user stories: 5 stories can be developed in parallel after Foundational
- Documentation: Can progress alongside implementation
**Independent Test Criteria per Story**:
- **US1**: Search flights → book with passengers → retrieve PNR → cancel booking (complete workflow)
- **US4**: Run 5-10 concurrent sessions, verify zero data leakage between sessions
- **US6**: Connect via Streamable HTTP → verify SSE polling → test MCP-Protocol-Version validation → test rate limiting
- **US2**: Search hotels → book hotel-only → bundle hotel with flight → retrieve multi-service PNR
- **US3**: Search cars → book car-only → add to flight+hotel → retrieve complete package
- **US5**: Review demo routes (JFK-LAX, JFK-LHR, LAX-TYO), verify professional data quality
**Suggested MVP Scope** (Minimum Viable Product):
- Phase 1: Setup (T001-T005)
- Phase 2: Foundational (T006-T014)
- Phase 3: User Story 1 (T015-T031)
- Phase 10: Docker packaging basics (T120-T125)
- Phase 11: Essential docs (T126-T127)
**Total MVP Tasks**: 34 tasks
**MVP delivers**: Fully functional flight search and booking mock server with Docker deployment
---
## Format Validation ✅
All tasks follow the required checklist format:
- ✅ Every task starts with `- [ ]` (markdown checkbox)
- ✅ Every task has sequential Task ID (T001, T002, T003...)
- ✅ User story tasks have [Story] label ([US1], [US2], [US3], [US4], [US5])
- ✅ Setup, Foundational, Docker, and Polish tasks have NO story label (correctly)
- ✅ Parallel tasks marked with [P]
- ✅ All descriptions include clear actions and exact file paths
- ✅ Tasks organized by phase with user stories as primary organization
- ✅ Each user story phase includes goal and independent test criteria
---
## Notes
- No tests included (not explicitly requested in spec.md per template guidelines)
- Tasks ordered for dependency flow: data → generators → tools → server integration
- Each user story can be deployed and tested independently
- MVP (US1 only) provides immediate value for flight booking testing
- Session management (US4) recommended early for CI/CD integration
- Multi-service bundling (US2, US3) extends functionality without breaking US1
- Demo data polish (US5) is final enhancement, not blocking
- Constitution compliance verification built into final phase
- All 8 MCP tools from contracts/mcp-tools.md covered across user stories

92
src/data/airlines.ts Normal file
View File

@@ -0,0 +1,92 @@
/**
* Airline mock data with IATA codes, names, and countries
* Covers 30+ major carriers worldwide
*/
export const airlines = [
// United States Carriers
{ code: 'AA', name: 'American Airlines', country: 'US', alliance: 'oneworld' },
{ code: 'DL', name: 'Delta Air Lines', country: 'US', alliance: 'SkyTeam' },
{ code: 'UA', name: 'United Airlines', country: 'US', alliance: 'Star Alliance' },
{ code: 'WN', name: 'Southwest Airlines', country: 'US', alliance: null },
{ code: 'B6', name: 'JetBlue Airways', country: 'US', alliance: null },
{ code: 'AS', name: 'Alaska Airlines', country: 'US', alliance: 'oneworld' },
{ code: 'F9', name: 'Frontier Airlines', country: 'US', alliance: null },
{ code: 'NK', name: 'Spirit Airlines', country: 'US', alliance: null },
// European Carriers
{ code: 'BA', name: 'British Airways', country: 'GB', alliance: 'oneworld' },
{ code: 'AF', name: 'Air France', country: 'FR', alliance: 'SkyTeam' },
{ code: 'LH', name: 'Lufthansa', country: 'DE', alliance: 'Star Alliance' },
{ code: 'KL', name: 'KLM Royal Dutch Airlines', country: 'NL', alliance: 'SkyTeam' },
{ code: 'IB', name: 'Iberia', country: 'ES', alliance: 'oneworld' },
{ code: 'AZ', name: 'ITA Airways', country: 'IT', alliance: 'SkyTeam' },
{ code: 'LX', name: 'Swiss International Air Lines', country: 'CH', alliance: 'Star Alliance' },
{ code: 'VS', name: 'Virgin Atlantic', country: 'GB', alliance: null },
// Asian Carriers
{ code: 'NH', name: 'All Nippon Airways', country: 'JP', alliance: 'Star Alliance' },
{ code: 'JL', name: 'Japan Airlines', country: 'JP', alliance: 'oneworld' },
{ code: 'SQ', name: 'Singapore Airlines', country: 'SG', alliance: 'Star Alliance' },
{ code: 'CX', name: 'Cathay Pacific', country: 'HK', alliance: 'oneworld' },
{ code: 'KE', name: 'Korean Air', country: 'KR', alliance: 'SkyTeam' },
{ code: 'OZ', name: 'Asiana Airlines', country: 'KR', alliance: 'Star Alliance' },
{ code: 'TG', name: 'Thai Airways', country: 'TH', alliance: 'Star Alliance' },
{ code: 'CA', name: 'Air China', country: 'CN', alliance: 'Star Alliance' },
{ code: 'MU', name: 'China Eastern Airlines', country: 'CN', alliance: 'SkyTeam' },
// Middle East Carriers
{ code: 'EK', name: 'Emirates', country: 'AE', alliance: null },
{ code: 'QR', name: 'Qatar Airways', country: 'QA', alliance: 'oneworld' },
{ code: 'EY', name: 'Etihad Airways', country: 'AE', alliance: null },
// Other Major Carriers
{ code: 'AC', name: 'Air Canada', country: 'CA', alliance: 'Star Alliance' },
{ code: 'QF', name: 'Qantas', country: 'AU', alliance: 'oneworld' },
{ code: 'NZ', name: 'Air New Zealand', country: 'NZ', alliance: 'Star Alliance' },
{ code: 'AM', name: 'Aeroméxico', country: 'MX', alliance: 'SkyTeam' },
{ code: 'LA', name: 'LATAM Airlines', country: 'CL', alliance: 'oneworld' }
];
/**
* Get airline by IATA code
* @param {string} code - IATA airline code (2 letters)
* @returns {Object|null} Airline object or null if not found
*/
export function getAirline(code: string) {
return airlines.find((a) => a.code === code.toUpperCase()) || null;
}
/**
* Get random airline for route generation
* @returns {Object} Random airline object
*/
export function getRandomAirline() {
return airlines[Math.floor(Math.random() * airlines.length)];
}
/**
* Check if airline code exists
* @param {string} code - IATA airline code
* @returns {boolean} True if airline exists
*/
export function isValidAirline(code: string) {
return getAirline(code) !== null;
}
/**
* Get airlines by alliance
* @param {string} alliance - Alliance name (oneworld, SkyTeam, Star Alliance)
* @returns {Object[]} Array of airline objects
*/
export function getAirlinesByAlliance(alliance: string) {
return airlines.filter((a) => a.alliance === alliance);
}
export default {
airlines,
getAirline,
getRandomAirline,
isValidAirline,
getAirlinesByAlliance
};

133
src/data/airports.ts Normal file
View File

@@ -0,0 +1,133 @@
/**
* Airport mock data with IATA codes, names, cities, timezones, and coordinates
* Covers 100+ major airports worldwide
*/
export const airports = [
// United States - Major Hubs
{ code: 'JFK', name: 'John F. Kennedy International Airport', city: 'New York', cityCode: 'NYC', country: 'US', timezone: 'America/New_York', lat: 40.6413, lon: -73.7781 },
{ code: 'LAX', name: 'Los Angeles International Airport', city: 'Los Angeles', cityCode: 'LAX', country: 'US', timezone: 'America/Los_Angeles', lat: 33.9416, lon: -118.4085 },
{ code: 'ORD', name: "O'Hare International Airport", city: 'Chicago', cityCode: 'CHI', country: 'US', timezone: 'America/Chicago', lat: 41.9742, lon: -87.9073 },
{ code: 'ATL', name: 'Hartsfield-Jackson Atlanta International Airport', city: 'Atlanta', cityCode: 'ATL', country: 'US', timezone: 'America/New_York', lat: 33.6407, lon: -84.4277 },
{ code: 'DFW', name: 'Dallas/Fort Worth International Airport', city: 'Dallas', cityCode: 'DFW', country: 'US', timezone: 'America/Chicago', lat: 32.8998, lon: -97.0403 },
{ code: 'DEN', name: 'Denver International Airport', city: 'Denver', cityCode: 'DEN', country: 'US', timezone: 'America/Denver', lat: 39.8561, lon: -104.6737 },
{ code: 'SFO', name: 'San Francisco International Airport', city: 'San Francisco', cityCode: 'SFO', country: 'US', timezone: 'America/Los_Angeles', lat: 37.6213, lon: -122.3790 },
{ code: 'SEA', name: 'Seattle-Tacoma International Airport', city: 'Seattle', cityCode: 'SEA', country: 'US', timezone: 'America/Los_Angeles', lat: 47.4502, lon: -122.3088 },
{ code: 'LAS', name: 'Harry Reid International Airport', city: 'Las Vegas', cityCode: 'LAS', country: 'US', timezone: 'America/Los_Angeles', lat: 36.0840, lon: -115.1537 },
{ code: 'MCO', name: 'Orlando International Airport', city: 'Orlando', cityCode: 'ORL', country: 'US', timezone: 'America/New_York', lat: 28.4312, lon: -81.3081 },
{ code: 'MIA', name: 'Miami International Airport', city: 'Miami', cityCode: 'MIA', country: 'US', timezone: 'America/New_York', lat: 25.7959, lon: -80.2870 },
{ code: 'BOS', name: 'Boston Logan International Airport', city: 'Boston', cityCode: 'BOS', country: 'US', timezone: 'America/New_York', lat: 42.3656, lon: -71.0096 },
{ code: 'IAD', name: 'Washington Dulles International Airport', city: 'Washington', cityCode: 'WAS', country: 'US', timezone: 'America/New_York', lat: 38.9531, lon: -77.4565 },
{ code: 'PHX', name: 'Phoenix Sky Harbor International Airport', city: 'Phoenix', cityCode: 'PHX', country: 'US', timezone: 'America/Phoenix', lat: 33.4352, lon: -112.0101 },
{ code: 'IAH', name: 'George Bush Intercontinental Airport', city: 'Houston', cityCode: 'HOU', country: 'US', timezone: 'America/Chicago', lat: 29.9902, lon: -95.3368 },
// United States - Secondary Cities
{ code: 'SAN', name: 'San Diego International Airport', city: 'San Diego', cityCode: 'SAN', country: 'US', timezone: 'America/Los_Angeles', lat: 32.7338, lon: -117.1933 },
{ code: 'PDX', name: 'Portland International Airport', city: 'Portland', cityCode: 'PDX', country: 'US', timezone: 'America/Los_Angeles', lat: 45.5898, lon: -122.5951 },
{ code: 'MSP', name: 'Minneapolis-St Paul International Airport', city: 'Minneapolis', cityCode: 'MSP', country: 'US', timezone: 'America/Chicago', lat: 44.8848, lon: -93.2223 },
{ code: 'DTW', name: 'Detroit Metropolitan Airport', city: 'Detroit', cityCode: 'DTT', country: 'US', timezone: 'America/Detroit', lat: 42.2162, lon: -83.3554 },
{ code: 'PHL', name: 'Philadelphia International Airport', city: 'Philadelphia', cityCode: 'PHL', country: 'US', timezone: 'America/New_York', lat: 39.8744, lon: -75.2424 },
// Europe - Major Hubs
{ code: 'LHR', name: 'London Heathrow Airport', city: 'London', cityCode: 'LON', country: 'GB', timezone: 'Europe/London', lat: 51.4700, lon: -0.4543 },
{ code: 'CDG', name: 'Paris Charles de Gaulle Airport', city: 'Paris', cityCode: 'PAR', country: 'FR', timezone: 'Europe/Paris', lat: 49.0097, lon: 2.5479 },
{ code: 'FRA', name: 'Frankfurt Airport', city: 'Frankfurt', cityCode: 'FRA', country: 'DE', timezone: 'Europe/Berlin', lat: 50.0379, lon: 8.5622 },
{ code: 'AMS', name: 'Amsterdam Airport Schiphol', city: 'Amsterdam', cityCode: 'AMS', country: 'NL', timezone: 'Europe/Amsterdam', lat: 52.3105, lon: 4.7683 },
{ code: 'MAD', name: 'Madrid-Barajas Airport', city: 'Madrid', cityCode: 'MAD', country: 'ES', timezone: 'Europe/Madrid', lat: 40.4983, lon: -3.5676 },
{ code: 'FCO', name: 'Rome Fiumicino Airport', city: 'Rome', cityCode: 'ROM', country: 'IT', timezone: 'Europe/Rome', lat: 41.8003, lon: 12.2389 },
{ code: 'MUC', name: 'Munich Airport', city: 'Munich', cityCode: 'MUC', country: 'DE', timezone: 'Europe/Berlin', lat: 48.3538, lon: 11.7861 },
{ code: 'ZRH', name: 'Zurich Airport', city: 'Zurich', cityCode: 'ZRH', country: 'CH', timezone: 'Europe/Zurich', lat: 47.4582, lon: 8.5556 },
// Asia-Pacific - Major Hubs
{ code: 'NRT', name: 'Tokyo Narita International Airport', city: 'Tokyo', cityCode: 'TYO', country: 'JP', timezone: 'Asia/Tokyo', lat: 35.7653, lon: 140.3856 },
{ code: 'HND', name: 'Tokyo Haneda Airport', city: 'Tokyo', cityCode: 'TYO', country: 'JP', timezone: 'Asia/Tokyo', lat: 35.5494, lon: 139.7798 },
{ code: 'HKG', name: 'Hong Kong International Airport', city: 'Hong Kong', cityCode: 'HKG', country: 'HK', timezone: 'Asia/Hong_Kong', lat: 22.3080, lon: 113.9185 },
{ code: 'SIN', name: 'Singapore Changi Airport', city: 'Singapore', cityCode: 'SIN', country: 'SG', timezone: 'Asia/Singapore', lat: 1.3644, lon: 103.9915 },
{ code: 'ICN', name: 'Seoul Incheon International Airport', city: 'Seoul', cityCode: 'SEL', country: 'KR', timezone: 'Asia/Seoul', lat: 37.4602, lon: 126.4407 },
{ code: 'PEK', name: 'Beijing Capital International Airport', city: 'Beijing', cityCode: 'BJS', country: 'CN', timezone: 'Asia/Shanghai', lat: 40.0799, lon: 116.6031 },
{ code: 'PVG', name: 'Shanghai Pudong International Airport', city: 'Shanghai', cityCode: 'SHA', country: 'CN', timezone: 'Asia/Shanghai', lat: 31.1443, lon: 121.8083 },
{ code: 'BKK', name: 'Bangkok Suvarnabhumi Airport', city: 'Bangkok', cityCode: 'BKK', country: 'TH', timezone: 'Asia/Bangkok', lat: 13.6900, lon: 100.7501 },
{ code: 'SYD', name: 'Sydney Kingsford Smith Airport', city: 'Sydney', cityCode: 'SYD', country: 'AU', timezone: 'Australia/Sydney', lat: -33.9399, lon: 151.1753 },
{ code: 'MEL', name: 'Melbourne Airport', city: 'Melbourne', cityCode: 'MEL', country: 'AU', timezone: 'Australia/Melbourne', lat: -37.6690, lon: 144.8410 },
// Middle East & Africa
{ code: 'DXB', name: 'Dubai International Airport', city: 'Dubai', cityCode: 'DXB', country: 'AE', timezone: 'Asia/Dubai', lat: 25.2532, lon: 55.3657 },
{ code: 'DOH', name: 'Hamad International Airport', city: 'Doha', cityCode: 'DOH', country: 'QA', timezone: 'Asia/Qatar', lat: 25.2609, lon: 51.6138 },
{ code: 'JNB', name: 'O.R. Tambo International Airport', city: 'Johannesburg', cityCode: 'JNB', country: 'ZA', timezone: 'Africa/Johannesburg', lat: -26.1392, lon: 28.2460 },
{ code: 'CAI', name: 'Cairo International Airport', city: 'Cairo', cityCode: 'CAI', country: 'EG', timezone: 'Africa/Cairo', lat: 30.1219, lon: 31.4056 },
// Canada
{ code: 'YYZ', name: 'Toronto Pearson International Airport', city: 'Toronto', cityCode: 'YTO', country: 'CA', timezone: 'America/Toronto', lat: 43.6777, lon: -79.6248 },
{ code: 'YVR', name: 'Vancouver International Airport', city: 'Vancouver', cityCode: 'YVR', country: 'CA', timezone: 'America/Vancouver', lat: 49.1967, lon: -123.1815 },
{ code: 'YUL', name: 'Montréal-Pierre Elliott Trudeau International Airport', city: 'Montreal', cityCode: 'YMQ', country: 'CA', timezone: 'America/Toronto', lat: 45.4706, lon: -73.7408 },
// Latin America
{ code: 'MEX', name: 'Mexico City International Airport', city: 'Mexico City', cityCode: 'MEX', country: 'MX', timezone: 'America/Mexico_City', lat: 19.4363, lon: -99.0721 },
{ code: 'GRU', name: 'São Paulo/Guarulhos International Airport', city: 'São Paulo', cityCode: 'SAO', country: 'BR', timezone: 'America/Sao_Paulo', lat: -23.4356, lon: -46.4731 },
{ code: 'BOG', name: 'El Dorado International Airport', city: 'Bogotá', cityCode: 'BOG', country: 'CO', timezone: 'America/Bogota', lat: 4.7016, lon: -74.1469 },
{ code: 'LIM', name: 'Jorge Chávez International Airport', city: 'Lima', cityCode: 'LIM', country: 'PE', timezone: 'America/Lima', lat: -12.0219, lon: -77.1143 }
];
/**
* Get airport by IATA code
* @param {string} code - IATA airport code (3 letters)
* @returns {Object|null} Airport object or null if not found
*/
export function getAirport(code: any) {
return airports.find((a) => a.code === code.toUpperCase()) || null;
}
/**
* Calculate great-circle distance between two airports in kilometers
* Uses Haversine formula
* @param {string} origin - Origin airport code
* @param {string} destination - Destination airport code
* @returns {number} Distance in kilometers
*/
export function calculateDistance(origin: any, destination: any) {
const from = getAirport(origin);
const to = getAirport(destination);
if (!from || !to) {
return 0;
}
const R = 6371; // Earth's radius in km
const dLat = toRad(to.lat - from.lat);
const dLon = toRad(to.lon - from.lon);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(from.lat)) * Math.cos(toRad(to.lat)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c;
return Math.round(distance);
}
/**
* Convert degrees to radians
* @param {number} degrees - Angle in degrees
* @returns {number} Angle in radians
*/
function toRad(degrees: any) {
return degrees * (Math.PI / 180);
}
/**
* Check if airport code exists
* @param {string} code - IATA airport code
* @returns {boolean} True if airport exists
*/
export function isValidAirport(code: any) {
return getAirport(code) !== null;
}
export default {
airports,
getAirport,
calculateDistance,
isValidAirport
};

138
src/data/cars.ts Normal file
View File

@@ -0,0 +1,138 @@
/**
* Car Rental Companies and Vehicle Mock Data
*/
export const carCompanies = [
{ code: 'HERTZ', name: 'Hertz', tier: 'premium' },
{ code: 'AVIS', name: 'Avis', tier: 'premium' },
{ code: 'BUDGET', name: 'Budget', tier: 'economy' },
{ code: 'ENTERPRISE', name: 'Enterprise', tier: 'standard' },
{ code: 'NATIONAL', name: 'National', tier: 'standard' },
{ code: 'ALAMO', name: 'Alamo', tier: 'economy' },
{ code: 'DOLLAR', name: 'Dollar', tier: 'economy' },
{ code: 'THRIFTY', name: 'Thrifty', tier: 'economy' }
];
export const carCategories = [
{ code: 'ECON', name: 'Economy', example: 'Toyota Yaris', passengers: 5, bags: 2, basePrice: 45 },
{ code: 'COMP', name: 'Compact', example: 'Honda Civic', passengers: 5, bags: 2, basePrice: 55 },
{ code: 'MID', name: 'Midsize', example: 'Toyota Camry', passengers: 5, bags: 3, basePrice: 65 },
{ code: 'FULL', name: 'Full-size', example: 'Chevrolet Impala', passengers: 5, bags: 4, basePrice: 75 },
{ code: 'SUV', name: 'SUV', example: 'Ford Explorer', passengers: 7, bags: 4, basePrice: 95 },
{ code: 'LUX', name: 'Luxury', example: 'BMW 5 Series', passengers: 5, bags: 3, basePrice: 150 },
{ code: 'VAN', name: 'Minivan', example: 'Honda Odyssey', passengers: 7, bags: 3, basePrice: 85 },
{ code: 'CONV', name: 'Convertible', example: 'Ford Mustang', passengers: 4, bags: 2, basePrice: 110 }
];
/**
* Generate car rental options
*/
export function generateCarOptions(pickupLocation: any, dropoffLocation: any, pickupDate: any, dropoffDate: any) {
const pickupDateObj = new Date(pickupDate);
const dropoffDateObj = new Date(dropoffDate);
const days = Math.ceil((dropoffDateObj.getTime() - pickupDateObj.getTime()) / (1000 * 60 * 60 * 24));
// Select 3-5 companies
const numCompanies = Math.floor(Math.random() * 3) + 3;
const selectedCompanies = carCompanies
.sort(() => Math.random() - 0.5)
.slice(0, numCompanies);
const options = [];
for (const company of selectedCompanies) {
// Each company offers 2-4 car categories
const numCategories = Math.floor(Math.random() * 3) + 2;
const selectedCategories = carCategories
.sort(() => Math.random() - 0.5)
.slice(0, numCategories);
for (const category of selectedCategories) {
// Apply company tier multiplier
const tierMultiplier = company.tier === 'premium' ? 1.15 : company.tier === 'economy' ? 0.9 : 1.0;
// Apply weekend/holiday multiplier
const dayOfWeek = pickupDateObj.getDay();
const weekendMultiplier = (dayOfWeek === 5 || dayOfWeek === 6) ? 1.2 : 1.0;
// One-way fee if different locations
const oneWayFee = pickupLocation !== dropoffLocation ? 75 : 0;
const dailyRate = Math.round(category.basePrice * tierMultiplier * weekendMultiplier);
const totalPrice = (dailyRate * days) + oneWayFee;
options.push({
carId: `CAR-${company.code}-${category.code}-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
company: company.name,
companyCode: company.code,
category: category.name,
categoryCode: category.code,
example: category.example,
passengers: category.passengers,
bags: category.bags,
features: generateFeatures(category.code, company.tier),
pricing: {
dailyRate,
days,
oneWayFee,
totalPrice,
currency: 'USD'
},
availability: 'available' // Mock: always available
});
}
}
return options.sort((a, b) => a.pricing.totalPrice - b.pricing.totalPrice);
}
/**
* Generate features based on category and tier
*/
function generateFeatures(categoryCode: any, tier: any) {
const baseFeatures = ['Automatic', 'Air Conditioning'];
if (tier === 'premium') {
baseFeatures.push('GPS', 'Bluetooth');
}
if (['SUV', 'VAN', 'LUX'].includes(categoryCode)) {
baseFeatures.push('Leather Seats');
}
if (categoryCode === 'LUX') {
baseFeatures.push('Premium Audio', 'Sunroof');
}
return baseFeatures;
}
/**
* Get car by ID (for booking validation)
*/
export function getCarById(carId: any) {
// Since cars are dynamically generated, we parse the ID
const [_, companyCode, categoryCode] = carId.split('-');
const company = carCompanies.find(c => c.code === companyCode);
const category = carCategories.find(c => c.code === categoryCode);
if (!company || !category) {
return null;
}
return {
carId,
company: company.name,
companyCode: company.code,
category: category.name,
categoryCode: category.code,
example: category.example,
passengers: category.passengers,
bags: category.bags,
features: generateFeatures(category.code, company.tier),
basePrice: category.basePrice
};
}
export default { carCompanies, carCategories, generateCarOptions, getCarById };

228
src/data/flights.ts Normal file
View File

@@ -0,0 +1,228 @@
import { getAirport, calculateDistance } from './airports.js';
import { getRandomAirline } from './airlines.js';
import { generateSegmentId } from './pnr.js';
/**
* Flight data generator with deterministic pricing, duration calculation, and availability logic
*/
/**
* Aircraft types by size category
*/
const aircraftTypes = {
shortHaul: ['Boeing 737-800', 'Airbus A320', 'Boeing 737 MAX 8', 'Airbus A321'],
mediumHaul: ['Boeing 757-200', 'Boeing 767-300', 'Airbus A330-200'],
longHaul: ['Boeing 777-300ER', 'Boeing 787-9', 'Airbus A350-900', 'Airbus A380']
};
/**
* Booking class codes by cabin
*/
const bookingClasses = {
economy: ['Y', 'B', 'M', 'H', 'Q', 'V', 'W'],
premium_economy: ['W', 'S', 'A'],
business: ['J', 'C', 'D', 'I', 'Z'],
first: ['F', 'A', 'P']
};
/**
* Generate mock flights for a search query
* @param {Object} params - Search parameters
* @param {string} params.origin - Origin airport code
* @param {string} params.destination - Destination airport code
* @param {string} params.departureDate - Departure date (YYYY-MM-DD)
* @param {Object} params.passengers - Passenger counts
* @param {string} params.cabin - Cabin class
* @returns {Object[]} Array of flight options
*/
export function generateFlights(params: any) {
const { origin, destination, departureDate, cabin = 'economy' } = params;
// Validate airports exist
const originAirport = getAirport(origin);
const destAirport = getAirport(destination);
if (!originAirport || !destAirport) {
return [];
}
// Calculate distance and flight characteristics
const distance = calculateDistance(origin, destination);
const duration = calculateFlightDuration(distance);
const aircraftType = selectAircraftType(distance);
// Generate 3-5 flight options
const flightCount = 3 + Math.floor(Math.random() * 3);
const flights = [];
// Generate flights at different times of day
const departureTimes = generateDepartureTimes(flightCount);
for (let i = 0; i < flightCount; i++) {
const airline = getRandomAirline();
const flightNumber = `${airline.code}${Math.floor(Math.random() * 900) + 100}`;
const departureTime = departureTimes[i];
const arrivalTime = calculateArrivalTime(departureTime, duration);
// Calculate pricing based on distance, cabin, and "availability"
const basePrice = calculateBasePrice(distance, cabin);
const priceVariation = 0.8 + Math.random() * 0.4; // ±20% variation
const price = Math.round(basePrice * priceVariation);
// Simulate availability (90% available, 10% sold out)
const isAvailable = Math.random() > 0.1;
const seatsAvailable = isAvailable ? Math.floor(Math.random() * 20) + 5 : 0;
const status = isAvailable ? 'available' : 'sold_out';
// Get booking class for this cabin
const bookingClass = bookingClasses[cabin][Math.floor(Math.random() * bookingClasses[cabin].length)];
const flight = {
id: generateSegmentId('flight', i),
flightNumber,
airlineCode: airline.code,
airlineName: airline.name,
originCode: origin,
originName: originAirport.name,
destinationCode: destination,
destinationName: destAirport.name,
departureTime: `${departureDate}T${departureTime}:00`,
arrivalTime: `${departureDate}T${arrivalTime}:00`,
duration,
aircraftType,
cabin,
price,
seatsAvailable,
bookingClass,
status,
metadata: {
distance,
data_source: 'mock'
}
};
flights.push(flight);
}
// Sort by departure time
flights.sort((a, b) => a.departureTime.localeCompare(b.departureTime));
return flights;
}
/**
* Calculate flight duration in minutes based on distance
* @param {number} distance - Distance in kilometers
* @returns {number} Duration in minutes
*/
function calculateFlightDuration(distance: any) {
// Average commercial aircraft speed: ~800 km/h
// Add taxi/boarding time: 30 minutes
const flightTime = (distance / 800) * 60;
const totalTime = flightTime + 30;
return Math.round(totalTime);
}
/**
* Select appropriate aircraft type based on distance
* @param {number} distance - Distance in kilometers
* @returns {string} Aircraft type
*/
function selectAircraftType(distance: any) {
if (distance < 2000) {
// Short-haul
return aircraftTypes.shortHaul[Math.floor(Math.random() * aircraftTypes.shortHaul.length)];
} else if (distance < 6000) {
// Medium-haul
return aircraftTypes.mediumHaul[Math.floor(Math.random() * aircraftTypes.mediumHaul.length)];
} else {
// Long-haul
return aircraftTypes.longHaul[Math.floor(Math.random() * aircraftTypes.longHaul.length)];
}
}
/**
* Calculate base price in USD cents based on distance and cabin
* @param {number} distance - Distance in kilometers
* @param {string} cabin - Cabin class
* @returns {number} Price in USD cents
*/
function calculateBasePrice(distance: any, cabin: any) {
// Base price per kilometer by cabin class
const pricePerKm = {
economy: 0.10, // $0.10/km
premium_economy: 0.14, // $0.14/km (+40%)
business: 0.30, // $0.30/km (+200%)
first: 0.50 // $0.50/km (+400%)
};
const rate = pricePerKm[cabin] || pricePerKm.economy;
const basePrice = distance * rate;
// Apply minimum prices
const minimums = {
economy: 200,
premium_economy: 300,
business: 800,
first: 2500
};
const minimum = minimums[cabin] || minimums.economy;
return Math.max(basePrice, minimum) * 100; // Convert to cents
}
/**
* Generate realistic departure times
* @param {number} count - Number of times to generate
* @returns {string[]} Array of departure times (HH:MM format)
*/
function generateDepartureTimes(count: any) {
// Common departure slots: 6am-10pm
const slots = [
'06:00', '06:30', '07:00', '07:30', '08:00', '08:30',
'09:00', '09:30', '10:00', '10:30', '11:00', '11:30',
'12:00', '12:30', '13:00', '13:30', '14:00', '14:30',
'15:00', '15:30', '16:00', '16:30', '17:00', '17:30',
'18:00', '18:30', '19:00', '19:30', '20:00', '20:30',
'21:00', '21:30', '22:00'
];
// Shuffle and take first N slots
const shuffled = slots.sort(() => Math.random() - 0.5);
return shuffled.slice(0, count).sort();
}
/**
* Calculate arrival time given departure time and duration
* @param {string} departureTime - Departure time (HH:MM)
* @param {number} duration - Duration in minutes
* @returns {string} Arrival time (HH:MM)
*/
function calculateArrivalTime(departureTime: any, duration: any) {
const [hours, minutes] = departureTime.split(':').map(Number);
const totalMinutes = hours * 60 + minutes + duration;
const arrivalHours = Math.floor(totalMinutes / 60) % 24;
const arrivalMinutes = totalMinutes % 60;
return `${String(arrivalHours).padStart(2, '0')}:${String(arrivalMinutes).padStart(2, '0')}`;
}
/**
* Get flight by ID from search results
* @param {string} flightId - Flight identifier
* @param {Object} searchParams - Original search parameters
* @returns {Object|null} Flight object or null
*/
export function getFlightById(flightId: any, searchParams: any) {
const flights = generateFlights(searchParams);
return flights.find((f) => f.id === flightId) || null;
}
export default {
generateFlights,
getFlightById,
calculateFlightDuration,
calculateBasePrice
};

127
src/data/hotels.ts Normal file
View File

@@ -0,0 +1,127 @@
/**
* Hotels Mock Data
* 50+ properties across major cities with realistic details
*/
export const hotels = [
// New York City
{ id: 'HTL001', name: 'Grand Manhattan Hotel', chain: 'Marriott', city: 'NYC', cityCode: 'JFK', stars: 5, address: '123 Fifth Avenue', amenities: ['WiFi', 'Parking', 'Breakfast', 'Gym', 'Pool', 'Spa'], basePrice: 450 },
{ id: 'HTL002', name: 'Times Square Inn', chain: 'Hilton', city: 'NYC', cityCode: 'JFK', stars: 4, address: '456 Broadway', amenities: ['WiFi', 'Gym', 'Restaurant'], basePrice: 320 },
{ id: 'HTL003', name: 'Brooklyn Budget Suites', chain: 'Independent', city: 'NYC', cityCode: 'JFK', stars: 3, address: '789 Brooklyn Ave', amenities: ['WiFi', 'Breakfast'], basePrice: 150 },
// Los Angeles
{ id: 'HTL004', name: 'Beverly Hills Grand', chain: 'Four Seasons', city: 'Los Angeles', cityCode: 'LAX', stars: 5, address: '100 Rodeo Drive', amenities: ['WiFi', 'Parking', 'Breakfast', 'Gym', 'Pool', 'Spa', 'Concierge'], basePrice: 550 },
{ id: 'HTL005', name: 'Santa Monica Beach Hotel', chain: 'Hyatt', city: 'Los Angeles', cityCode: 'LAX', stars: 4, address: '200 Ocean Ave', amenities: ['WiFi', 'Pool', 'Gym', 'Restaurant'], basePrice: 280 },
{ id: 'HTL006', name: 'Downtown LA Comfort', chain: 'Holiday Inn', city: 'Los Angeles', cityCode: 'LAX', stars: 3, address: '300 Main St', amenities: ['WiFi', 'Parking', 'Breakfast'], basePrice: 120 },
// London
{ id: 'HTL007', name: 'The Royal Westminster', chain: 'Intercontinental', city: 'London', cityCode: 'LHR', stars: 5, address: '10 Piccadilly', amenities: ['WiFi', 'Breakfast', 'Gym', 'Spa', 'Concierge'], basePrice: 480 },
{ id: 'HTL008', name: 'Kensington Palace Hotel', chain: 'Marriott', city: 'London', cityCode: 'LHR', stars: 4, address: '20 Kensington Rd', amenities: ['WiFi', 'Gym', 'Restaurant'], basePrice: 310 },
{ id: 'HTL009', name: 'East End Express', chain: 'Premier Inn', city: 'London', cityCode: 'LHR', stars: 3, address: '30 Whitechapel', amenities: ['WiFi', 'Breakfast'], basePrice: 135 },
// Tokyo
{ id: 'HTL010', name: 'Shibuya Imperial', chain: 'Prince Hotels', city: 'Tokyo', cityCode: 'TYO', stars: 5, address: '1-1 Shibuya', amenities: ['WiFi', 'Breakfast', 'Gym', 'Pool', 'Spa'], basePrice: 420 },
{ id: 'HTL011', name: 'Shinjuku Business Hotel', chain: 'APA Hotels', city: 'Tokyo', cityCode: 'TYO', stars: 3, address: '2-2 Shinjuku', amenities: ['WiFi', 'Restaurant'], basePrice: 140 },
// Paris
{ id: 'HTL012', name: 'Le Grand Paris', chain: 'Sofitel', city: 'Paris', cityCode: 'CDG', stars: 5, address: '1 Avenue des Champs', amenities: ['WiFi', 'Breakfast', 'Gym', 'Spa', 'Concierge'], basePrice: 500 },
{ id: 'HTL013', name: 'Montmartre Boutique', chain: 'Independent', city: 'Paris', cityCode: 'CDG', stars: 4, address: '15 Rue Montmartre', amenities: ['WiFi', 'Breakfast'], basePrice: 250 },
// San Francisco
{ id: 'HTL014', name: 'Union Square Luxury', chain: 'Westin', city: 'San Francisco', cityCode: 'SFO', stars: 5, address: '50 Union Square', amenities: ['WiFi', 'Gym', 'Pool', 'Spa'], basePrice: 420 },
{ id: 'HTL015', name: 'Fisherman\'s Wharf Inn', chain: 'Best Western', city: 'San Francisco', cityCode: 'SFO', stars: 3, address: '100 Jefferson St', amenities: ['WiFi', 'Parking'], basePrice: 160 },
// Miami
{ id: 'HTL016', name: 'South Beach Resort', chain: 'Fontainebleau', city: 'Miami', cityCode: 'MIA', stars: 5, address: '1000 Ocean Drive', amenities: ['WiFi', 'Pool', 'Gym', 'Spa', 'Beach'], basePrice: 380 },
{ id: 'HTL017', name: 'Coral Gables Hotel', chain: 'Marriott', city: 'Miami', cityCode: 'MIA', stars: 4, address: '200 Miracle Mile', amenities: ['WiFi', 'Pool', 'Gym'], basePrice: 220 },
// Chicago
{ id: 'HTL018', name: 'Magnificent Mile Tower', chain: 'Trump Hotels', city: 'Chicago', cityCode: 'ORD', stars: 5, address: '401 Michigan Ave', amenities: ['WiFi', 'Gym', 'Spa', 'Restaurant'], basePrice: 390 },
{ id: 'HTL019', name: 'Loop Business Center', chain: 'Hyatt', city: 'Chicago', cityCode: 'ORD', stars: 4, address: '100 State St', amenities: ['WiFi', 'Gym'], basePrice: 195 },
// Dubai
{ id: 'HTL020', name: 'Burj Al Arab', chain: 'Jumeirah', city: 'Dubai', cityCode: 'DXB', stars: 5, address: 'Jumeirah Beach', amenities: ['WiFi', 'Pool', 'Gym', 'Spa', 'Beach', 'Concierge'], basePrice: 800 },
{ id: 'HTL021', name: 'Marina Bay Hotel', chain: 'Hilton', city: 'Dubai', cityCode: 'DXB', stars: 4, address: 'Dubai Marina', amenities: ['WiFi', 'Pool', 'Gym'], basePrice: 280 },
// Singapore
{ id: 'HTL022', name: 'Marina Bay Sands', chain: 'Independent', city: 'Singapore', cityCode: 'SIN', stars: 5, address: '10 Bayfront Ave', amenities: ['WiFi', 'Pool', 'Gym', 'Spa', 'Casino'], basePrice: 480 },
{ id: 'HTL023', name: 'Orchard Road Plaza', chain: 'Shangri-La', city: 'Singapore', cityCode: 'SIN', stars: 4, address: '22 Orchard Rd', amenities: ['WiFi', 'Pool', 'Gym'], basePrice: 260 },
// Additional cities for diversity
{ id: 'HTL024', name: 'Sydney Harbour Hotel', chain: 'Four Seasons', city: 'Sydney', cityCode: 'SYD', stars: 5, address: '199 George St', amenities: ['WiFi', 'Pool', 'Gym', 'Spa'], basePrice: 410 },
{ id: 'HTL025', name: 'Vegas Strip Mega Resort', chain: 'MGM', city: 'Las Vegas', cityCode: 'LAS', stars: 5, address: '3799 Las Vegas Blvd', amenities: ['WiFi', 'Pool', 'Gym', 'Casino', 'Spa'], basePrice: 320 },
{ id: 'HTL026', name: 'Seattle Downtown Suites', chain: 'Hyatt', city: 'Seattle', cityCode: 'SEA', stars: 4, address: '1001 Pike St', amenities: ['WiFi', 'Gym'], basePrice: 230 },
{ id: 'HTL027', name: 'Boston Harbor Hotel', chain: 'Marriott', city: 'Boston', cityCode: 'BOS', stars: 4, address: '70 Rowes Wharf', amenities: ['WiFi', 'Gym', 'Restaurant'], basePrice: 275 },
{ id: 'HTL028', name: 'Atlanta Peachtree Plaza', chain: 'Westin', city: 'Atlanta', cityCode: 'ATL', stars: 4, address: '210 Peachtree St', amenities: ['WiFi', 'Pool', 'Gym'], basePrice: 185 },
{ id: 'HTL029', name: 'Denver Mountain View', chain: 'Hilton', city: 'Denver', cityCode: 'DEN', stars: 4, address: '1701 Broadway', amenities: ['WiFi', 'Gym'], basePrice: 195 },
{ id: 'HTL030', name: 'Phoenix Desert Resort', chain: 'JW Marriott', city: 'Phoenix', cityCode: 'PHX', stars: 5, address: '5350 E Marriott Dr', amenities: ['WiFi', 'Pool', 'Gym', 'Spa', 'Golf'], basePrice: 340 },
// More international cities
{ id: 'HTL031', name: 'Rome Colosseum View', chain: 'St. Regis', city: 'Rome', cityCode: 'FCO', stars: 5, address: 'Via Vittorio', amenities: ['WiFi', 'Breakfast', 'Gym', 'Spa'], basePrice: 450 },
{ id: 'HTL032', name: 'Barcelona Ramblas Hotel', chain: 'Independent', city: 'Barcelona', cityCode: 'BCN', stars: 4, address: 'La Rambla 45', amenities: ['WiFi', 'Breakfast'], basePrice: 210 },
{ id: 'HTL033', name: 'Amsterdam Canal House', chain: 'NH Hotels', city: 'Amsterdam', cityCode: 'AMS', stars: 4, address: 'Prinsengracht 100', amenities: ['WiFi', 'Breakfast'], basePrice: 240 },
{ id: 'HTL034', name: 'Hong Kong Harbor Plaza', chain: 'Mandarin Oriental', city: 'Hong Kong', cityCode: 'HKG', stars: 5, address: '5 Connaught Rd', amenities: ['WiFi', 'Pool', 'Gym', 'Spa'], basePrice: 490 },
{ id: 'HTL035', name: 'Bangkok Sukhumvit Suites', chain: 'Sofitel', city: 'Bangkok', cityCode: 'BKK', stars: 4, address: '189 Sukhumvit Rd', amenities: ['WiFi', 'Pool', 'Gym'], basePrice: 180 },
// US Regional coverage
{ id: 'HTL036', name: 'Nashville Music City Hotel', chain: 'Gaylord', city: 'Nashville', cityCode: 'BNA', stars: 4, address: '2800 Opryland Dr', amenities: ['WiFi', 'Pool', 'Gym'], basePrice: 205 },
{ id: 'HTL037', name: 'Austin Downtown', chain: 'Fairmont', city: 'Austin', cityCode: 'AUS', stars: 4, address: '101 Red River', amenities: ['WiFi', 'Pool', 'Gym'], basePrice: 220 },
{ id: 'HTL038', name: 'Portland Pearl District', chain: 'Kimpton', city: 'Portland', cityCode: 'PDX', stars: 4, address: '425 NW 9th Ave', amenities: ['WiFi', 'Gym', 'Restaurant'], basePrice: 215 },
{ id: 'HTL039', name: 'Philadelphia Historic Inn', chain: 'Independent', city: 'Philadelphia', cityCode: 'PHL', stars: 3, address: '1234 Market St', amenities: ['WiFi', 'Breakfast'], basePrice: 165 },
{ id: 'HTL040', name: 'Detroit Renaissance Center', chain: 'Marriott', city: 'Detroit', cityCode: 'DTW', stars: 4, address: '400 Renaissance Dr', amenities: ['WiFi', 'Gym'], basePrice: 170 },
// European additions
{ id: 'HTL041', name: 'Berlin Mitte Palace', chain: 'Adlon', city: 'Berlin', cityCode: 'BER', stars: 5, address: 'Unter den Linden 77', amenities: ['WiFi', 'Spa', 'Gym'], basePrice: 380 },
{ id: 'HTL042', name: 'Munich Marienplatz', chain: 'Bayerischer Hof', city: 'Munich', cityCode: 'MUC', stars: 5, address: 'Promenadeplatz 2', amenities: ['WiFi', 'Pool', 'Spa'], basePrice: 400 },
{ id: 'HTL043', name: 'Zurich Lake View', chain: 'Baur au Lac', city: 'Zurich', cityCode: 'ZRH', stars: 5, address: 'Talstrasse 1', amenities: ['WiFi', 'Spa', 'Restaurant'], basePrice: 520 },
{ id: 'HTL044', name: 'Vienna Imperial', chain: 'Imperial', city: 'Vienna', cityCode: 'VIE', stars: 5, address: 'Kärntner Ring 16', amenities: ['WiFi', 'Spa', 'Restaurant'], basePrice: 430 },
{ id: 'HTL045', name: 'Brussels Grand Place', chain: 'Amigo', city: 'Brussels', cityCode: 'BRU', stars: 5, address: 'Rue de l\'Amigo 1', amenities: ['WiFi', 'Restaurant'], basePrice: 350 },
// Asia Pacific additions
{ id: 'HTL046', name: 'Seoul Gangnam Suites', chain: 'Park Hyatt', city: 'Seoul', cityCode: 'ICN', stars: 5, address: '606 Teheran-ro', amenities: ['WiFi', 'Pool', 'Gym', 'Spa'], basePrice: 360 },
{ id: 'HTL047', name: 'Shanghai Bund Hotel', chain: 'Peninsula', city: 'Shanghai', cityCode: 'PVG', stars: 5, address: '32 The Bund', amenities: ['WiFi', 'Pool', 'Spa'], basePrice: 420 },
{ id: 'HTL048', name: 'Mumbai Marine Drive', chain: 'Taj', city: 'Mumbai', cityCode: 'BOM', stars: 5, address: 'Apollo Bunder', amenities: ['WiFi', 'Pool', 'Spa'], basePrice: 280 },
{ id: 'HTL049', name: 'Melbourne CBD Tower', chain: 'Crown', city: 'Melbourne', cityCode: 'MEL', stars: 5, address: '8 Whiteman St', amenities: ['WiFi', 'Pool', 'Gym'], basePrice: 330 },
{ id: 'HTL050', name: 'Osaka Namba Plaza', chain: 'Swissotel', city: 'Osaka', cityCode: 'KIX', stars: 4, address: '5-1-60 Namba', amenities: ['WiFi', 'Gym'], basePrice: 195 }
];
/**
* Get hotels by city code
*/
export function getHotelsByCity(cityCode: any) {
return hotels.filter(h => h.cityCode === cityCode);
}
/**
* Get hotel by ID
*/
export function getHotelById(hotelId: any) {
return hotels.find(h => h.id === hotelId);
}
/**
* Generate hotel pricing based on check-in date, nights, and base price
*/
export function generateHotelPrice(basePrice: any, nights: any, checkInDate: any) {
const date = new Date(checkInDate);
const dayOfWeek = date.getDay();
// Weekend premium (Friday-Saturday)
const weekendMultiplier = (dayOfWeek === 5 || dayOfWeek === 6) ? 1.3 : 1.0;
// Seasonal variation (simple month-based)
const month = date.getMonth();
const peakSeason = [5, 6, 7, 11]; // June, July, Aug, Dec
const seasonMultiplier = peakSeason.includes(month) ? 1.2 : 1.0;
const pricePerNight = Math.round(basePrice * weekendMultiplier * seasonMultiplier);
const totalPrice = pricePerNight * nights;
return {
pricePerNight,
nights,
totalPrice
};
}
export default hotels;

104
src/data/pnr.ts Normal file
View File

@@ -0,0 +1,104 @@
/**
* PNR (Passenger Name Record) generation utilities
* Format: TEST-{BASE32} (e.g., TEST-ABC123)
*/
const BASE32_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/**
* Generate a unique PNR with TEST- prefix
* @param {string} sessionId - Session identifier for entropy
* @returns {string} PNR in format TEST-XXXXXX
*/
export function generatePNR(sessionId = '') {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000000);
// Combine session, timestamp, and random for uniqueness
const entropy = `${sessionId}${timestamp}${random}`;
// Generate base32 encoded string
const code = generateBase32(entropy, 6);
return `TEST-${code}`;
}
/**
* Generate base32 encoded string from input
* @param {string} input - Input string for entropy
* @param {number} length - Desired output length (default: 6)
* @returns {string} Base32 encoded string
*/
function generateBase32(input, length = 6) {
let result = '';
let hash = simpleHash(input);
for (let i = 0; i < length; i++) {
const index = hash % 32;
result += BASE32_CHARS[index];
hash = Math.floor(hash / 32) + (hash % 32);
// Add more entropy if hash gets too small
if (hash < 32) {
hash = simpleHash(result + Date.now());
}
}
return result;
}
/**
* Simple hash function for string input
* @param {string} str - Input string
* @returns {number} Hash value
*/
function simpleHash(str: any) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash);
}
/**
* Validate PNR format
* @param {string} pnr - PNR to validate
* @returns {boolean} True if valid format
*/
export function isValidPNR(pnr: any) {
return /^TEST-[A-Z0-9]{6}$/.test(pnr);
}
/**
* Extract session-scoped booking ID from PNR
* For display and tracking purposes
* @param {string} pnr - PNR code
* @returns {string} Booking ID (just the code part)
*/
export function extractBookingId(pnr: any) {
if (!isValidPNR(pnr)) {
return pnr;
}
return pnr.replace('TEST-', '');
}
/**
* Generate a unique segment ID
* @param {string} type - Segment type (flight, hotel, car)
* @param {number} index - Segment index
* @returns {string} Segment identifier
*/
export function generateSegmentId(type: any, index: any) {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000);
return `${type}-${index}-${timestamp}-${random}`;
}
export default {
generatePNR,
isValidPNR,
extractBookingId,
generateSegmentId
};

438
src/index.ts Normal file
View 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();

49
src/middleware/cors.ts Normal file
View File

@@ -0,0 +1,49 @@
import cors from 'cors';
import { logger } from '../utils/logger.js';
/**
* CORS middleware with permissive wildcard policy for development
* Per MCP specification: MUST validate Origin if present, 403 if invalid
*/
export const corsMiddleware = cors({
origin: '*', // Wildcard policy for development/testing
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'MCP-Session-Id',
'MCP-Protocol-Version',
'Last-Event-ID',
],
exposedHeaders: ['Mcp-Session-Id', 'Mcp-Protocol-Version'],
credentials: false,
maxAge: 86400 // 24 hours
});
/**
* Origin validation per MCP security requirement
* Responds with 403 if Origin is present but invalid
*/
export function validateOrigin(req: any, res: any, next: any) {
const origin = req.get('Origin');
// If no Origin header, allow (stdio-like clients)
if (!origin) {
return next();
}
// Validate Origin (currently permissive for testing)
// In production, implement stricter validation
const allowedOrigins = process.env.CORS_ORIGINS?.split(',') || ['*'];
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin)) {
return next();
}
logger.warn({ origin }, 'Origin validation failed');
return res.status(403).json({
error: 'Forbidden',
message: 'Invalid Origin'
});
}
export default corsMiddleware;

35
src/middleware/logger.ts Normal file
View File

@@ -0,0 +1,35 @@
import { logger as baseLogger } from '../utils/logger.js';
/**
* Request logging middleware
* Logs method, path, IP, User-Agent, MCP headers, duration, status code
*/
export function requestLoggerMiddleware(req: any, res: any, next: any) {
const startTime = Date.now();
// Skip health check logging (too noisy)
if (req.path === '/health') {
return next();
}
// Log on response finish
res.on('finish', () => {
const duration = Date.now() - startTime;
const logger = baseLogger.child({ request: true });
logger.info({
method: req.method,
path: req.path,
ip: req.ip,
userAgent: req.get('User-Agent'),
mcpProtocolVersion: req.get('MCP-Protocol-Version'),
mcpSessionId: req.get('MCP-Session-Id'),
statusCode: res.statusCode,
duration
}, 'HTTP request completed');
});
next();
}
export default requestLoggerMiddleware;

View File

@@ -0,0 +1,30 @@
import { logger } from '../utils/logger.js';
/**
* Middleware to ensure MCP messages have proper structure
* Works around SDK issues with missing optional fields
*/
export function messageNormalizationMiddleware(req: any, res: any, next: any) {
// Skip for health checks (middleware is mounted on /mcp, so paths are relative)
if (req.path === '/health') {
return next();
}
// Only process POST requests with JSON body
if (req.method !== 'POST' || !req.body) {
return next();
}
const message = req.body;
// If it's a notification (no id field) and params is missing, add empty params
// Note: Must check if 'id' property exists, not just truthiness (id:0 is valid!)
if (message.method && !('id' in message) && !message.params) {
logger.debug({ method: message.method }, 'Adding empty params to notification');
req.body.params = {};
}
next();
}
export default messageNormalizationMiddleware;

View File

@@ -0,0 +1,56 @@
import { logger } from '../utils/logger.js';
// Supported MCP protocol versions (current stable + backward compatibility)
const SUPPORTED_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26'];
/**
* MCP Protocol Version validation middleware
* Per MCP Streamable HTTP specification 2025-06-18:
* - MCP-Protocol-Version header is NOT required during initialization
* - MCP-Protocol-Version header IS required on all SUBSEQUENT requests after initialization
* - If missing (and not initialization), reject with 400 Bad Request
* - If present but invalid/unsupported, reject with 400 Bad Request
*/
export function protocolVersionMiddleware(req: any, res: any, next: any) {
// Skip for health checks
if (req.path === '/health') {
return next();
}
const protocolVersion = req.get('MCP-Protocol-Version');
// Check if this is an initialization request by examining the request body
// The initialize request has method: "initialize"
const isInitializeRequest = req.body && req.body.method === 'initialize';
// Header is NOT required during initialization, but IS required for all subsequent requests
if (!protocolVersion) {
// Allow initialization requests without the header
if (isInitializeRequest) {
logger.debug('Initialize request - MCP-Protocol-Version header not required');
return next();
}
// For non-initialization requests, the header is required
logger.warn({ path: req.path, method: req.body?.method }, 'Missing MCP-Protocol-Version header on non-initialization request');
return res.status(400).json({
error: 'Bad Request',
message: 'MCP-Protocol-Version header is required'
});
}
// Validate version is supported
if (!SUPPORTED_VERSIONS.includes(protocolVersion)) {
logger.warn({ version: protocolVersion }, 'Unsupported MCP protocol version');
return res.status(400).json({
error: 'Bad Request',
message: `Unsupported MCP protocol version: ${protocolVersion}. Supported versions: ${SUPPORTED_VERSIONS.join(', ')}`
});
}
logger.debug({ protocolVersion }, 'protocolVersionMiddleware passed - valid version');
// Version valid, proceed
next();
}
export default protocolVersionMiddleware;

View File

@@ -0,0 +1,69 @@
import rateLimit from 'express-rate-limit';
import { storage } from '../session/storage.js';
import { logger } from '../utils/logger.js';
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '100', 10);
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute in milliseconds
/**
* Rate limiter using Valkey for distributed rate limiting
* Default: 100 requests per minute per IP
*/
export const rateLimitMiddleware = rateLimit({
windowMs: RATE_LIMIT_WINDOW,
max: RATE_LIMIT_MAX,
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
// Use Valkey for distributed storage
store: {
async increment(key) {
const client = storage.client;
const current = await client.incr(key);
if (current === 1) {
await client.expire(key, 60); // Set TTL on first increment
}
return {
totalHits: current,
resetTime: new Date(Date.now() + 60000)
};
},
async decrement(key) {
const client = storage.client;
await client.decr(key);
},
async resetKey(key) {
const client = storage.client;
await client.del(key);
}
},
// Use default key generator (handles IPv6 properly)
// Removed custom keyGenerator to use express-rate-limit's built-in IP handling
// Handler for rate limit exceeded
handler: (req, res) => {
const retryAfter = Math.ceil(RATE_LIMIT_WINDOW / 1000);
logger.warn({ ip: req.ip }, 'Rate limit exceeded');
res.status(429)
.set('Retry-After', retryAfter.toString())
.json({
error: 'Too Many Requests',
message: `Rate limit exceeded. Try again in ${retryAfter} seconds`,
retryAfter
});
},
skip: (req) => {
// Skip rate limiting for health checks
return req.path === '/health';
}
});
export default rateLimitMiddleware;

50
src/remote/config.ts Normal file
View File

@@ -0,0 +1,50 @@
/**
* Remote Access Configuration
* Environment-based configuration for Streamable HTTP transport (HTTP/1.1 + SSE)
* Per MCP specification 2025-06-18
*/
export const remoteConfig = {
// Server configuration
port: parseInt(process.env.PORT || '3000', 10),
host: process.env.HOST || '127.0.0.1', // Bind to localhost by default per MCP security
// Transport mode
transport: process.env.TRANSPORT || 'stdio', // 'stdio' or 'http'
// Rate limiting
rateLimit: {
enabled: process.env.RATE_LIMIT_ENABLED !== 'false',
maxRequests: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
windowMs: 60000 // 1 minute (fixed per spec)
},
// CORS configuration
cors: {
enabled: process.env.CORS_ENABLED !== 'false',
origins: process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(',') : ['*'],
credentials: false // Disabled for wildcard CORS
},
// MCP Protocol Version - using current stable spec version
mcpProtocolVersion: '2025-06-18',
// Session configuration
sessionTtl: parseInt(process.env.MCP_SESSION_TIMEOUT || '3600', 10), // 1 hour default
// PNR TTL configuration
pnrTtl: parseInt(process.env.PNR_TTL || '3600', 10), // 1 hour default
// Logging
logLevel: process.env.LOG_LEVEL || 'info',
// Health check
healthPath: '/health',
// SSE configuration
sse: {
retryMs: parseInt(process.env.SSE_RETRY_MS || '5000', 10) // Retry field value
}
};
export default remoteConfig;

56
src/remote/health.ts Normal file
View File

@@ -0,0 +1,56 @@
import { storage } from '../session/storage.js';
import { logger } from '../utils/logger.js';
/**
* Health check handler
* Returns service status, Valkey connection, active sessions, uptime
*/
export async function healthCheck(req: any, res: any) {
try {
const client = storage.client;
// Test Valkey connection
let valkeyStatus = 'disconnected';
let activeSessions = 0;
try {
await client.ping();
valkeyStatus = 'connected';
// Get active session count
const sessionKeys = await client.keys('gds:session:*');
activeSessions = sessionKeys.length;
} catch (err) {
logger.error({ err }, 'Health check: Valkey connection failed');
}
const health = {
status: valkeyStatus === 'connected' ? 'healthy' : 'degraded',
service: 'gds-mock-mcp',
version: '0.1.0',
uptime: Math.floor(process.uptime()),
timestamp: new Date().toISOString(),
valkey: {
status: valkeyStatus,
activeSessions
},
memory: {
rss: Math.floor(process.memoryUsage().rss / 1024 / 1024),
heapUsed: Math.floor(process.memoryUsage().heapUsed / 1024 / 1024)
}
};
// Return 503 if Valkey is disconnected
const statusCode = valkeyStatus === 'connected' ? 200 : 503;
res.status(statusCode).json(health);
} catch (err) {
logger.error({ err }, 'Health check failed');
res.status(500).json({
status: 'unhealthy',
error: err.message
});
}
}
export default { healthCheck };

155
src/server.ts Normal file
View File

@@ -0,0 +1,155 @@
import { McpServer } from '@modelcontextprotocol/server';
import { storage } from './session/storage.js';
import { sessionManager } from './session/manager.js';
import { logger, logToolCall, logToolResponse, logError } from './utils/logger.js';
import { formatErrorResponse } from './utils/errors.js';
/**
* Mock GDS MCP Server
* Exposes GDS operations as MCP tools using McpServer from @modelcontextprotocol/server
*/
export class GDSMockServer {
server: any;
sessionId: string | null;
constructor() {
// Create MCP server instance
this.server = new McpServer(
{
name: 'gds-mock-mcp',
version: '0.1.0'
},
{
capabilities: {
logging: {},
tools: {}
}
}
);
this.sessionId = null;
}
/**
* Register a tool handler using McpServer's registerTool API
* @param {string} toolName - Tool name
* @param {string} title - Human-readable tool title
* @param {string} description - Tool description
* @param {Object} inputSchema - Zod schema or JSON Schema for tool input
* @param {Function} handler - Handler function (args, sessionId) => Promise
*/
registerTool(toolName: string, title: string, description: string, inputSchema: any, handler: any) {
// Register tool with McpServer
this.server.registerTool(
toolName,
{
title,
description,
inputSchema
},
async (args: any, _ctx: any) => {
const startTime = Date.now();
try {
// Ensure session exists
if (!this.sessionId) {
const session = await sessionManager.createSession();
this.sessionId = session.id;
logger.info({ sessionId: this.sessionId }, 'Session created for MCP connection');
}
// Update session activity
await sessionManager.updateActivity(this.sessionId);
// Log tool call
logToolCall(toolName, args, this.sessionId);
// Execute tool with session context
const result = await handler(args, this.sessionId);
// Log success
const duration = Date.now() - startTime;
logToolResponse(toolName, duration, this.sessionId, true, { resultType: typeof result });
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
} catch (error) {
// Log error
const duration = Date.now() - startTime;
logError(error as Error, {
tool: toolName,
sessionId: this.sessionId,
duration
});
logToolResponse(toolName, duration, this.sessionId, false, {
error: (error as Error).message
});
// Format error response
const errorResponse = formatErrorResponse(error as Error);
return {
content: [
{
type: 'text',
text: JSON.stringify(errorResponse, null, 2)
}
],
isError: true
};
}
}
);
logger.info({ tool: toolName, title }, 'Tool registered');
}
/**
* Initialize storage connection
*/
async initStorage() {
try {
await storage.connect();
logger.info('Storage connected successfully');
} catch (error) {
logError(error, { context: 'storage_init' });
throw error;
}
}
/**
* Connect to transport
* @param {Transport} transport - MCP transport instance
*/
async connect(transport) {
try {
await this.server.connect(transport);
logger.info('MCP server connected to transport');
} catch (error) {
logError(error, { context: 'server_connect' });
throw error;
}
}
/**
* Stop the MCP server
*/
async stop() {
try {
await storage.disconnect();
await this.server.close();
logger.info('GDS Mock MCP Server stopped');
} catch (error) {
logError(error, { context: 'server_stop' });
throw error;
}
}
}
export default GDSMockServer;

240
src/session/manager.ts Normal file
View File

@@ -0,0 +1,240 @@
import { storage } from './storage.js';
import { logger } from '../utils/logger.js';
import { SessionError } from '../utils/errors.js';
/**
* Session lifecycle manager with TTL management
* Default TTL: 1 hour (3600 seconds)
*/
const DEFAULT_SESSION_TTL = parseInt(process.env.MCP_SESSION_TIMEOUT || '3600', 10);
/**
* Session Manager class
*/
export class SessionManager {
defaultTTL: number;
constructor() {
this.defaultTTL = DEFAULT_SESSION_TTL;
}
/**
* Create a new session
* @returns {Promise<Object>} Session object with id and metadata
*/
async createSession() {
const sessionId = this.generateSessionId();
const now = Date.now();
const expiresAt = now + (this.defaultTTL * 1000);
const sessionData = {
id: sessionId,
createdAt: now.toString(),
expiresAt: expiresAt.toString(),
lastActivity: now.toString(),
bookingCount: '0',
searchCount: '0'
};
const sessionKey = this.getSessionKey(sessionId);
try {
await storage.hmset(sessionKey, sessionData);
await storage.expire(sessionKey, this.defaultTTL);
// Add to active sessions set
await storage.sadd('gds:stats:sessions:active', sessionId);
logger.info({
type: 'session_created',
sessionId,
ttl: this.defaultTTL
});
return {
id: sessionId,
createdAt: now,
expiresAt,
ttl: this.defaultTTL
};
} catch (error) {
logger.error({ error, sessionId }, 'Failed to create session');
throw error;
}
}
/**
* Get session data
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} Session data
* @throws {SessionError} If session not found or expired
*/
async getSession(sessionId) {
const sessionKey = this.getSessionKey(sessionId);
try {
const exists = await storage.exists(sessionKey);
if (!exists) {
throw new SessionError('Session not found or expired', true);
}
const sessionData = await storage.hgetall(sessionKey);
return {
id: sessionId,
createdAt: parseInt(sessionData.createdAt, 10),
expiresAt: parseInt(sessionData.expiresAt, 10),
lastActivity: parseInt(sessionData.lastActivity, 10),
bookingCount: parseInt(sessionData.bookingCount, 10),
searchCount: parseInt(sessionData.searchCount, 10)
};
} catch (error) {
if (error instanceof SessionError) {
throw error;
}
logger.error({ error, sessionId }, 'Failed to get session');
throw error;
}
}
/**
* Validate session exists and is not expired
* @param {string} sessionId - Session identifier
* @returns {Promise<boolean>} True if valid
* @throws {SessionError} If session invalid or expired
*/
async validateSession(sessionId) {
await this.getSession(sessionId); // Will throw if invalid
return true;
}
/**
* Update session activity timestamp (refreshes TTL)
* @param {string} sessionId - Session identifier
* @returns {Promise<void>}
*/
async updateActivity(sessionId) {
const sessionKey = this.getSessionKey(sessionId);
const now = Date.now();
try {
await storage.hset(sessionKey, 'lastActivity', now.toString());
await storage.expire(sessionKey, this.defaultTTL);
logger.debug({
type: 'session_activity_updated',
sessionId,
timestamp: now
});
} catch (error) {
logger.error({ error, sessionId }, 'Failed to update session activity');
throw error;
}
}
/**
* Increment booking counter for session
* @param {string} sessionId - Session identifier
* @returns {Promise<number>} New booking count
*/
async incrementBookingCount(sessionId) {
const sessionKey = this.getSessionKey(sessionId);
try {
const sessionData = await storage.hgetall(sessionKey);
const newCount = parseInt(sessionData.bookingCount || '0', 10) + 1;
await storage.hset(sessionKey, 'bookingCount', newCount.toString());
return newCount;
} catch (error) {
logger.error({ error, sessionId }, 'Failed to increment booking count');
throw error;
}
}
/**
* Increment search counter for session
* @param {string} sessionId - Session identifier
* @returns {Promise<number>} New search count
*/
async incrementSearchCount(sessionId) {
const sessionKey = this.getSessionKey(sessionId);
try {
const sessionData = await storage.hgetall(sessionKey);
const newCount = parseInt(sessionData.searchCount || '0', 10) + 1;
await storage.hset(sessionKey, 'searchCount', newCount.toString());
return newCount;
} catch (error) {
logger.error({ error, sessionId }, 'Failed to increment search count');
throw error;
}
}
/**
* End session (delete from storage)
* @param {string} sessionId - Session identifier
* @returns {Promise<void>}
*/
async endSession(sessionId) {
const sessionKey = this.getSessionKey(sessionId);
try {
await storage.del(sessionKey);
await storage.srem('gds:stats:sessions:active', sessionId);
logger.info({
type: 'session_ended',
sessionId
});
} catch (error) {
logger.error({ error, sessionId }, 'Failed to end session');
throw error;
}
}
/**
* Generate unique session ID
* @returns {string} UUID v4 session ID
*/
generateSessionId() {
// Using crypto.randomUUID() which is available in Node.js 20+
return crypto.randomUUID();
}
/**
* Get Valkey key for session
* @param {string} sessionId - Session identifier
* @returns {string} Valkey key
*/
getSessionKey(sessionId) {
return `gds:session:${sessionId}`;
}
/**
* Get Valkey key for session bookings set
* @param {string} sessionId - Session identifier
* @returns {string} Valkey key
*/
getBookingsKey(sessionId) {
return `gds:session:${sessionId}:bookings`;
}
/**
* Get Valkey key for specific booking
* @param {string} sessionId - Session identifier
* @param {string} pnr - PNR code
* @returns {string} Valkey key
*/
getBookingKey(sessionId, pnr) {
return `gds:session:${sessionId}:booking:${pnr}`;
}
}
// Export singleton instance
export const sessionManager = new SessionManager();
export default sessionManager;

289
src/session/storage.ts Normal file
View File

@@ -0,0 +1,289 @@
import { Redis } from 'ioredis';
import { logger } from '../utils/logger.js';
import { StorageError } from '../utils/errors.js';
import { log } from 'node:console';
/**
* Valkey client wrapper with connection pooling and error handling
*/
class ValkeyStorage {
client: any;
isConnected: boolean;
events: Map<string, any>;
constructor() {
this.client = null;
this.isConnected = false;
// Event store for MCP resumability (Map<string, { streamId: string, message: object }>)
this.events = new Map();
}
/**
* Initialize Valkey connection
* @param {Object} config - Valkey configuration
* @returns {Promise<void>}
*/
async connect(config = {}) {
logger.info({ config }, 'Initializing Valkey connection with config');
const defaultConfig = {
host: process.env.VALKEY_HOST || 'localhost',
port: parseInt(process.env.VALKEY_PORT || '6379', 10),
password: process.env.VALKEY_PASSWORD || undefined,
db: parseInt(process.env.VALKEY_DB || '0', 10),
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
maxRetriesPerRequest: 3,
enableReadyCheck: true,
lazyConnect: false
};
const finalConfig = { ...defaultConfig, ...config };
try {
this.client = new Redis(finalConfig);
this.client.on('connect', () => {
logger.info('Valkey client connecting...');
});
this.client.on('ready', () => {
this.isConnected = true;
logger.info('Valkey client connected and ready');
});
this.client.on('error', (err) => {
logger.error({ error: err }, 'Valkey client error');
this.isConnected = false;
});
this.client.on('close', () => {
logger.warn('Valkey connection closed');
this.isConnected = false;
});
this.client.on('reconnecting', () => {
logger.info('Valkey client reconnecting...');
});
// Wait for connection to be ready
await this.client.ping();
logger.info('Valkey connection established successfully');
} catch (error) {
logger.error({ error }, 'Failed to connect to Valkey');
throw new StorageError('Failed to connect to Valkey', { error: error.message });
}
}
/**
* Close Valkey connection
*/
async disconnect() {
if (this.client) {
await this.client.quit();
this.isConnected = false;
logger.info('Valkey client disconnected');
}
}
/**
* Set a key-value pair
* @param {string} key - Key
* @param {string} value - Value
* @param {number} ttl - Time to live in seconds (optional)
* @returns {Promise<string>}
*/
async set(key, value, ttl = null) {
try {
if (ttl) {
return await this.client.setex(key, ttl, value);
}
return await this.client.set(key, value);
} catch (error) {
throw new StorageError(`Failed to set key '${key}'`, { error: error.message });
}
}
/**
* Get value by key
* @param {string} key - Key
* @returns {Promise<string|null>}
*/
async get(key) {
try {
return await this.client.get(key);
} catch (error) {
throw new StorageError(`Failed to get key '${key}'`, { error: error.message });
}
}
/**
* Delete a key
* @param {string} key - Key
* @returns {Promise<number>} Number of keys deleted
*/
async del(key) {
try {
return await this.client.del(key);
} catch (error) {
throw new StorageError(`Failed to delete key '${key}'`, { error: error.message });
}
}
/**
* Check if key exists
* @param {string} key - Key
* @returns {Promise<boolean>}
*/
async exists(key) {
try {
const result = await this.client.exists(key);
return result === 1;
} catch (error) {
throw new StorageError(`Failed to check existence of key '${key}'`, { error: error.message });
}
}
/**
* Set hash field
* @param {string} key - Hash key
* @param {string} field - Field name
* @param {string} value - Field value
* @returns {Promise<number>}
*/
async hset(key, field, value) {
try {
return await this.client.hset(key, field, value);
} catch (error) {
throw new StorageError(`Failed to set hash field '${field}' in '${key}'`, { error: error.message });
}
}
/**
* Get hash field
* @param {string} key - Hash key
* @param {string} field - Field name
* @returns {Promise<string|null>}
*/
async hget(key, field) {
try {
return await this.client.hget(key, field);
} catch (error) {
throw new StorageError(`Failed to get hash field '${field}' from '${key}'`, { error: error.message });
}
}
/**
* Get all hash fields
* @param {string} key - Hash key
* @returns {Promise<Object>}
*/
async hgetall(key) {
try {
return await this.client.hgetall(key);
} catch (error) {
throw new StorageError(`Failed to get all hash fields from '${key}'`, { error: error.message });
}
}
/**
* Set multiple hash fields
* @param {string} key - Hash key
* @param {Object} data - Field-value pairs
* @returns {Promise<string>}
*/
async hmset(key, data) {
try {
return await this.client.hmset(key, data);
} catch (error) {
throw new StorageError(`Failed to set multiple hash fields in '${key}'`, { error: error.message });
}
}
/**
* Add member to set
* @param {string} key - Set key
* @param {string} member - Member to add
* @returns {Promise<number>}
*/
async sadd(key, member) {
try {
return await this.client.sadd(key, member);
} catch (error) {
throw new StorageError(`Failed to add member to set '${key}'`, { error: error.message });
}
}
/**
* Remove member from set
* @param {string} key - Set key
* @param {string} member - Member to remove
* @returns {Promise<number>}
*/
async srem(key, member) {
try {
return await this.client.srem(key, member);
} catch (error) {
throw new StorageError(`Failed to remove member from set '${key}'`, { error: error.message });
}
}
/**
* Get all set members
* @param {string} key - Set key
* @returns {Promise<string[]>}
*/
async smembers(key) {
try {
return await this.client.smembers(key);
} catch (error) {
throw new StorageError(`Failed to get members from set '${key}'`, { error: error.message });
}
}
/**
* Set TTL on key
* @param {string} key - Key
* @param {number} seconds - TTL in seconds
* @returns {Promise<number>}
*/
async expire(key, seconds) {
try {
return await this.client.expire(key, seconds);
} catch (error) {
throw new StorageError(`Failed to set TTL on key '${key}'`, { error: error.message });
}
}
/**
* Increment counter
* @param {string} key - Key
* @returns {Promise<number>}
*/
async incr(key) {
try {
return await this.client.incr(key);
} catch (error) {
throw new StorageError(`Failed to increment key '${key}'`, { error: error.message });
}
}
/**
* Get multiple keys by pattern
* @param {string} pattern - Key pattern (e.g., 'gds:session:*')
* @returns {Promise<string[]>}
*/
async keys(pattern) {
try {
return await this.client.keys(pattern);
} catch (error) {
throw new StorageError(`Failed to get keys matching pattern '${pattern}'`, { error: error.message });
}
}
}
// Export singleton instance
export const storage = new ValkeyStorage();
export default storage;

View File

@@ -0,0 +1,104 @@
import { storage } from './storage.js';
import { logger } from '../utils/logger.js';
import type { EventStore, JSONRPCMessage } from '@modelcontextprotocol/server';
/**
* Valkey-based Event Store for MCP resumability
* Stores server-sent events for reconnection support
*/
export class ValkeyEventStore implements EventStore {
prefix: string;
ttl: number;
constructor() {
this.prefix = 'mcp:events:';
this.ttl = 3600; // Events expire after 1 hour
}
/**
* Generates a unique event ID for a given stream ID
*/
private generateEventId(streamId: string): number {
return Date.now();
}
/**
* Extracts the stream ID from an event ID
*/
private getStreamIdFromEventId(eventId: string): string {
const parts = eventId.split('_');
return parts.length > 0 ? parts[0]! : '';
}
/**
* Stores an event with a generated event ID
* Implements EventStore.storeEvent
*/
async storeEvent(streamId: string, message: JSONRPCMessage): Promise<string> {
logger.debug({ streamId, message }, 'Storing event in ValkeyEventStore');
const eventId = this.generateEventId(streamId);
const key = `${this.prefix}${streamId}`;
const value = JSON.stringify({ eventId, message, timestamp: Date.now() });
// Add to sorted set with event ID as score for ordering
await storage.client.zadd(key, eventId, value);
// Set expiration on the key
await storage.client.expire(key, this.ttl);
return "" + eventId;
}
/**
* Replays events that occurred after a specific event ID
* Implements EventStore.replayEventsAfter
*/
async replayEventsAfter(
lastEventId: string,
{ send }: { send: (eventId: string, message: JSONRPCMessage) => Promise<void> }
): Promise<string> {
const streamId = this.getStreamIdFromEventId(lastEventId);
const key = `${this.prefix}${streamId}`;
// Retrieve all events from the sorted set that come after lastEventId
// Using ZRANGEBYSCORE to get events with scores > lastEventId
const events = await storage.client.zrangebyscore(
key,
`(${lastEventId}`, // Exclusive range - events after lastEventId
'+inf' // Up to the highest score
);
if (!events || events.length === 0) {
logger.debug({ streamId, lastEventId }, 'No events to replay');
return lastEventId;
}
let latestEventId = lastEventId;
// Replay each event in order
for (const eventData of events) {
try {
const parsed = JSON.parse(eventData);
const { eventId, message } = parsed;
// Send the event using the provided callback
await send(eventId, message);
latestEventId = eventId;
logger.debug({ streamId, eventId }, 'Event replayed');
} catch (error) {
logger.error({ error, eventData }, 'Failed to replay event');
// Continue with next event even if one fails
}
}
logger.debug(
{ streamId, lastEventId, latestEventId, count: events.length },
'Event replay completed'
);
return latestEventId;
}
}
export default ValkeyEventStore;

244
src/tools/bookings.ts Normal file
View File

@@ -0,0 +1,244 @@
import { storage } from '../session/storage.js';
import { sessionManager } from '../session/manager.js';
import { logger } from '../utils/logger.js';
import {
BookingError,
validatePNR
} from '../utils/errors.js';
import {
validateRequired,
validateNumber
} from '../validation/validators.js';
/**
* Retrieve booking by PNR
* @param {Object} params - Parameters
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} Booking details
*/
export async function retrieveBooking(params: any, sessionId: any) {
try {
// Validate required fields
validateRequired(params, ['pnr']);
// Validate PNR format
validatePNR(params.pnr);
// Retrieve booking from global PNR storage first
const globalBookingKey = `gds:pnr:${params.pnr}`;
let bookingData = await storage.get(globalBookingKey);
// Fallback to session-scoped storage if not found globally
if (!bookingData) {
const bookingKey = sessionManager.getBookingKey(sessionId, params.pnr);
bookingData = await storage.get(bookingKey);
}
if (!bookingData) {
throw new BookingError(`Booking ${params.pnr} not found`);
}
const booking = JSON.parse(bookingData);
logger.info({
type: 'booking_retrieved',
sessionId,
pnr: params.pnr
});
return {
success: true,
booking,
metadata: {
sessionId,
timestamp: new Date().toISOString()
}
};
} catch (error) {
logger.error({ error, sessionId, pnr: params.pnr }, 'Booking retrieval failed');
throw error;
}
}
/**
* Cancel booking
* @param {Object} params - Parameters
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} Cancellation confirmation
*/
export async function cancelBooking(params: any, sessionId: any) {
try {
// Validate required fields
validateRequired(params, ['pnr']);
// Validate PNR format
validatePNR(params.pnr);
// Retrieve booking from global PNR storage first
const globalBookingKey = `gds:pnr:${params.pnr}`;
let bookingData = await storage.get(globalBookingKey);
let bookingKey = globalBookingKey;
// Fallback to session-scoped storage if not found globally
if (!bookingData) {
const sessionBookingKey = sessionManager.getBookingKey(sessionId, params.pnr);
bookingData = await storage.get(sessionBookingKey);
bookingKey = sessionBookingKey;
}
if (!bookingData) {
throw new BookingError(`Booking ${params.pnr} not found`);
}
const booking = JSON.parse(bookingData);
// Check if already cancelled
if (booking.status === 'cancelled') {
throw new BookingError('Booking is already cancelled', true);
}
// Update booking status
booking.status = 'cancelled';
booking.lastModified = Date.now();
booking.cancelledAt = Date.now();
// Store updated booking in both locations
await storage.set(bookingKey, JSON.stringify(booking));
// Also update global storage if bookingKey is not already the global key
if (bookingKey !== globalBookingKey) {
await storage.set(globalBookingKey, JSON.stringify(booking));
}
// Remove from session bookings set
const bookingsSetKey = sessionManager.getBookingsKey(sessionId);
await storage.srem(bookingsSetKey, params.pnr);
logger.info({
type: 'booking_cancelled',
sessionId,
pnr: params.pnr
});
return {
success: true,
pnr: params.pnr,
status: 'cancelled',
message: `Booking ${params.pnr} has been cancelled successfully`,
cancelledAt: booking.cancelledAt,
metadata: {
sessionId,
timestamp: new Date().toISOString()
}
};
} catch (error) {
logger.error({ error, sessionId, pnr: params.pnr }, 'Booking cancellation failed');
throw error;
}
}
/**
* List all bookings in current session
* @param {Object} params - Parameters
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} List of bookings
*/
export async function listBookings(params: any, sessionId: any) {
try {
// Validate pagination parameters
const limit = params.limit || 10;
const offset = params.offset || 0;
validateNumber(limit, 'limit', 1, 100);
validateNumber(offset, 'offset', 0);
// Get all booking PNRs for this session
const bookingsSetKey = sessionManager.getBookingsKey(sessionId);
const pnrs = await storage.smembers(bookingsSetKey);
if (pnrs.length === 0) {
return {
success: true,
bookings: [],
total: 0,
limit,
offset,
metadata: {
sessionId,
timestamp: new Date().toISOString()
}
};
}
// Retrieve booking summaries
const bookingSummaries = [];
for (const pnr of pnrs) {
// Try global storage first
const globalBookingKey = `gds:pnr:${pnr}`;
let bookingData = await storage.get(globalBookingKey);
// Fallback to session-scoped storage
if (!bookingData) {
const bookingKey = sessionManager.getBookingKey(sessionId, pnr);
bookingData = await storage.get(bookingKey);
}
if (bookingData) {
const booking = JSON.parse(bookingData);
// Create summary
const summary = {
pnr: booking.pnr,
status: booking.status,
createdAt: booking.createdAt,
lastModified: booking.lastModified,
totalPrice: booking.totalPrice,
currency: booking.currency,
segments: {
flights: booking.flights.length,
hotels: booking.hotels.length,
cars: booking.cars.length
},
passengerCount: booking.passengers.length
};
bookingSummaries.push(summary);
}
}
// Sort by creation date (newest first)
bookingSummaries.sort((a, b) => b.createdAt - a.createdAt);
// Apply pagination
const paginatedBookings = bookingSummaries.slice(offset, offset + limit);
logger.info({
type: 'bookings_listed',
sessionId,
total: bookingSummaries.length,
returned: paginatedBookings.length
});
return {
success: true,
bookings: paginatedBookings,
total: bookingSummaries.length,
limit,
offset,
metadata: {
sessionId,
timestamp: new Date().toISOString()
}
};
} catch (error) {
logger.error({ error, sessionId }, 'Booking list failed');
throw error;
}
}
export default {
retrieveBooking,
cancelBooking,
listBookings
};

270
src/tools/cars.ts Normal file
View File

@@ -0,0 +1,270 @@
/**
* Car Rental Tools
* Implements searchCars and bookCar MCP tools
*/
import { generateCarOptions, getCarById } from '../data/cars.js';
import { generatePNR } from '../data/pnr.js';
import { storage } from '../session/storage.js';
import { logger } from '../utils/logger.js';
import { ValidationError, BookingError } from '../utils/errors.js';
import {
validateRequired,
validateString,
validateFutureDate
} from '../validation/validators.js';
/**
* Search for available car rentals
* @param {Object} params - Search parameters
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} Search results
*/
export async function searchCars(params: any, sessionId: any) {
try {
// Validate required fields
validateRequired(params.pickupLocation, 'pickupLocation');
validateRequired(params.dropoffLocation, 'dropoffLocation');
validateRequired(params.pickupDate, 'pickupDate');
validateRequired(params.dropoffDate, 'dropoffDate');
validateString(params.pickupLocation, 'pickupLocation', { minLength: 3, maxLength: 3 });
validateString(params.dropoffLocation, 'dropoffLocation', { minLength: 3, maxLength: 3 });
validateFutureDate(params.pickupDate, 'pickupDate');
validateFutureDate(params.dropoffDate, 'dropoffDate');
const pickupLocation = params.pickupLocation.toUpperCase();
const dropoffLocation = params.dropoffLocation.toUpperCase();
const pickupDate = new Date(params.pickupDate);
const dropoffDate = new Date(params.dropoffDate);
// Validate date range
if (dropoffDate <= pickupDate) {
throw new ValidationError('dropoffDate must be after pickupDate');
}
const dropoffDateObj = new Date(dropoffDate);
const days = Math.ceil((dropoffDateObj.getTime() - pickupDate.getTime()) / (1000 * 60 * 60 * 24));
if (days < 1) {
throw new ValidationError('Minimum rental period is 1 day');
}
// Generate car options
const options = generateCarOptions(
pickupLocation,
dropoffLocation,
params.pickupDate,
params.dropoffDate
);
logger.info({ sessionId, pickupLocation, dropoffLocation, days, resultCount: options.length }, 'Car search completed');
return {
pickupLocation,
dropoffLocation,
pickupDate: params.pickupDate,
dropoffDate: params.dropoffDate,
days,
results: options
};
} catch (error) {
logger.error({ error, sessionId }, 'Car search failed');
throw error;
}
}
/**
* Book a car rental
* @param {Object} params - Booking parameters
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} Booking confirmation
*/
export async function bookCar(params: any, sessionId: any) {
try {
// Validate required fields
validateRequired(params.carId, 'carId');
validateRequired(params.pickupLocation, 'pickupLocation');
validateRequired(params.dropoffLocation, 'dropoffLocation');
validateRequired(params.pickupDate, 'pickupDate');
validateRequired(params.dropoffDate, 'dropoffDate');
validateRequired(params.driverName, 'driverName');
validateRequired(params.driverEmail, 'driverEmail');
const { carId, pickupLocation, dropoffLocation, pickupDate, dropoffDate, driverName, driverEmail, driverPhone, driverLicense, pnr: existingPnr } = params;
// Validate car exists
const car = getCarById(carId);
if (!car) {
throw new ValidationError(`Car not found: ${carId}`);
}
// Validate dates
validateFutureDate(pickupDate, 'pickupDate');
validateFutureDate(dropoffDate, 'dropoffDate');
const pickup = new Date(pickupDate);
const dropoff = new Date(dropoffDate);
if (dropoff <= pickup) {
throw new ValidationError('dropoffDate must be after pickupDate');
}
const pickupDateObj = new Date(params.pickupDate);
const dropoffDateObj = new Date(params.dropoffDate);
const days = Math.ceil((dropoffDateObj.getTime() - pickupDateObj.getTime()) / (1000 * 60 * 60 * 24));
// Calculate pricing (recalculate with same logic as search)
const oneWayFee = pickupLocation !== dropoffLocation ? 75 : 0;
const dailyRate = car.basePrice; // Simplified - real calculation in generateCarOptions
const totalPrice = (dailyRate * days) + oneWayFee;
// Generate or use existing PNR
const pnr = existingPnr || generatePNR();
const client = storage.client;
// Check if this is adding to existing booking
let booking;
if (existingPnr) {
const existingKey = `gds:session:${sessionId}:booking:${existingPnr}`;
const existingData = await client.get(existingKey);
if (!existingData) {
throw new BookingError(`Booking not found: ${existingPnr}`);
}
booking = JSON.parse(existingData);
// Add car segment
if (!booking.cars) {
booking.cars = [];
}
booking.cars.push({
carId: car.carId,
company: car.company,
category: car.category,
example: car.example,
passengers: car.passengers,
bags: car.bags,
features: car.features,
pickupLocation,
dropoffLocation,
pickupDate,
dropoffDate,
days,
driverName,
driverEmail,
driverPhone,
driverLicense,
pricing: {
dailyRate,
days,
oneWayFee,
totalPrice,
currency: 'USD'
},
status: 'confirmed',
bookedAt: new Date().toISOString()
});
// Recalculate total price
let totalBookingPrice = 0;
if (booking.flights) {
totalBookingPrice += booking.flights.reduce((sum, f) => sum + (f.pricing?.totalPrice || 0), 0);
}
if (booking.hotels) {
totalBookingPrice += booking.hotels.reduce((sum, h) => sum + (h.pricing?.totalPrice || 0), 0);
}
if (booking.cars) {
totalBookingPrice += booking.cars.reduce((sum, c) => sum + (c.pricing?.totalPrice || 0), 0);
}
booking.totalPrice = totalBookingPrice;
booking.updatedAt = new Date().toISOString();
} else {
// Create new car-only booking
booking = {
pnr,
type: 'car',
status: 'confirmed',
cars: [{
carId: car.carId,
company: car.company,
category: car.category,
example: car.example,
passengers: car.passengers,
bags: car.bags,
features: car.features,
pickupLocation,
dropoffLocation,
pickupDate,
dropoffDate,
days,
driverName,
driverEmail,
driverPhone,
driverLicense,
pricing: {
dailyRate,
days,
oneWayFee,
totalPrice,
currency: 'USD'
},
status: 'confirmed',
bookedAt: new Date().toISOString()
}],
totalPrice,
currency: 'USD',
createdAt: new Date().toISOString(),
sessionId
};
}
// Store booking
const bookingKey = `gds:session:${sessionId}:booking:${pnr}`;
await client.set(bookingKey, JSON.stringify(booking), 'EX', 3600);
// Update session bookings set
await client.sadd(`gds:session:${sessionId}:bookings`, pnr);
logger.info({ sessionId, pnr, carId, days }, 'Car rental booking created');
return {
pnr,
status: 'confirmed',
car: {
carId: car.carId,
company: car.company,
category: car.category,
example: car.example,
pickupLocation,
dropoffLocation,
pickupDate,
dropoffDate,
days
},
pricing: {
dailyRate,
days,
oneWayFee,
totalPrice,
currency: 'USD'
},
driverName,
driverEmail,
message: existingPnr
? `Car rental added to existing booking ${pnr}`
: `Car rental confirmed with PNR ${pnr}`
};
} catch (error) {
logger.error({ error, sessionId }, 'Car booking failed');
throw error;
}
}
export default { searchCars, bookCar };

296
src/tools/flights.ts Normal file
View File

@@ -0,0 +1,296 @@
import { generateFlights } from '../data/flights.js';
import { generatePNR } from '../data/pnr.js';
import { storage } from '../session/storage.js';
import { sessionManager } from '../session/manager.js';
import { logger } from '../utils/logger.js';
import {
BookingError,
ValidationError,
validateAirportCode,
validateDate,
validatePNR
} from '../utils/errors.js';
import {
validateRequired,
validateString,
validateNumber,
validateArray,
validateFutureDate,
validatePassenger,
validateCabin
} from '../validation/validators.js';
/**
* Search for available flights
* @param {Object} params - Search parameters
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} Search results
*/
export async function searchFlights(params: any, sessionId: any) {
try {
// Validate required fields
validateRequired(params, ['origin', 'destination', 'departureDate']);
// Validate airport codes
validateAirportCode(params.origin);
validateAirportCode(params.destination);
// Validate same origin/destination
if (params.origin === params.destination) {
throw new ValidationError('Origin and destination must be different');
}
// Validate departure date
validateDate(params.departureDate, 'departureDate');
validateFutureDate(params.departureDate, 'departureDate');
// Validate cabin class if provided
if (params.cabin) {
validateCabin(params.cabin);
}
// Validate passengers
const passengers = params.passengers || { adults: 1, children: 0, infants: 0 };
if (passengers.adults) {
validateNumber(passengers.adults, 'passengers.adults', 1, 9);
}
if (passengers.children) {
validateNumber(passengers.children, 'passengers.children', 0, 9);
}
if (passengers.infants) {
validateNumber(passengers.infants, 'passengers.infants', 0, 9);
}
// Generate flight results
const flights = generateFlights({
origin: params.origin.toUpperCase(),
destination: params.destination.toUpperCase(),
departureDate: params.departureDate,
passengers,
cabin: params.cabin || 'economy'
});
// Increment search counter
await sessionManager.incrementSearchCount(sessionId);
logger.info({
type: 'flight_search',
sessionId,
origin: params.origin,
destination: params.destination,
resultCount: flights.length
});
return {
search: {
origin: params.origin.toUpperCase(),
destination: params.destination.toUpperCase(),
departureDate: params.departureDate,
passengers,
cabin: params.cabin || 'economy'
},
results: flights,
resultCount: flights.length,
metadata: {
sessionId,
timestamp: new Date().toISOString()
}
};
} catch (error) {
logger.error({ error, sessionId }, 'Flight search failed');
throw error;
}
}
/**
* Book a flight
* @param {Object} params - Booking parameters
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} Booking confirmation
*/
export async function bookFlight(params: any, sessionId: any) {
try {
// Validate required fields
validateRequired(params, ['flightId', 'passengers']);
// Validate passengers array
validateArray(params.passengers, 'passengers', 1, 9);
// Validate each passenger
for (const passenger of params.passengers) {
validatePassenger(passenger);
}
// Validate contact information
if (params.contactEmail) {
validateString(params.contactEmail, 'contactEmail', 5, 100);
}
if (params.contactPhone) {
validateString(params.contactPhone, 'contactPhone', 10, 20);
}
// Ensure at least one contact method
if (!params.contactEmail && !params.contactPhone) {
// Use first passenger's email if available
if (params.passengers[0].email) {
params.contactEmail = params.passengers[0].email;
} else {
throw new ValidationError('At least one contact method (email or phone) is required');
}
}
// Check if adding to existing PNR
let pnr;
let existingBooking = null;
if (params.pnr) {
// Validate PNR format
validatePNR(params.pnr);
// Retrieve existing booking
const bookingKey = sessionManager.getBookingKey(sessionId, params.pnr);
const bookingData = await storage.get(bookingKey);
if (!bookingData) {
throw new BookingError(`Booking ${params.pnr} not found in current session`);
}
existingBooking = JSON.parse(bookingData);
if (existingBooking.status === 'cancelled') {
throw new BookingError('Cannot add to cancelled booking', true);
}
pnr = params.pnr;
} else {
// Generate new PNR
pnr = generatePNR(sessionId);
}
// Create flight segment from provided details or flight ID
// For mock implementation, we expect the caller to provide the full flight object
// In production, we would retrieve from a database or cache
const flightSegment = params.flightDetails || {
id: params.flightId,
// Basic mock data if details not provided
flightNumber: 'MOCK001',
airlineCode: 'XX',
airlineName: 'Mock Airline',
originCode: 'XXX',
originName: 'Unknown',
destinationCode: 'YYY',
destinationName: 'Unknown',
departureTime: new Date().toISOString(),
arrivalTime: new Date(Date.now() + 3600000).toISOString(),
duration: 60,
aircraftType: 'Mock Aircraft',
cabin: 'economy',
price: 20000, // $200.00
seatsAvailable: 10,
bookingClass: 'Y',
status: 'available'
};
// Create or update booking
const booking = existingBooking || {
pnr,
sessionId,
createdAt: Date.now(),
lastModified: Date.now(),
status: 'confirmed',
passengers: params.passengers,
flights: [],
hotels: [],
cars: [],
totalPrice: 0,
currency: 'USD',
contactEmail: params.contactEmail || '',
contactPhone: params.contactPhone || ''
};
// Add flight segment
booking.flights.push(flightSegment);
booking.lastModified = Date.now();
// Calculate total price
booking.totalPrice = calculateTotalPrice(booking);
// Store booking in both session-scoped AND global storage
const bookingKey = sessionManager.getBookingKey(sessionId, pnr);
await storage.set(bookingKey, JSON.stringify(booking));
// Store in global PNR namespace with TTL (1 hour default)
const globalBookingKey = `gds:pnr:${pnr}`;
const ttlSeconds = parseInt(process.env.PNR_TTL || '3600', 10);
await storage.set(globalBookingKey, JSON.stringify(booking), ttlSeconds);
// Add to session bookings set
const bookingsSetKey = sessionManager.getBookingsKey(sessionId);
await storage.sadd(bookingsSetKey, pnr);
// Increment booking counter
await sessionManager.incrementBookingCount(sessionId);
logger.info({
type: 'flight_booked',
sessionId,
pnr,
passengerCount: params.passengers.length
});
return {
success: true,
pnr,
status: booking.status,
booking: {
pnr,
passengers: booking.passengers,
flights: booking.flights,
totalPrice: booking.totalPrice,
currency: booking.currency,
createdAt: booking.createdAt,
contactEmail: booking.contactEmail,
contactPhone: booking.contactPhone
},
message: `Flight booked successfully. PNR: ${pnr}`,
metadata: {
sessionId,
timestamp: new Date().toISOString()
}
};
} catch (error) {
logger.error({ error, sessionId }, 'Flight booking failed');
throw error;
}
}
/**
* Calculate total price across all booking segments
* @param {Object} booking - Booking object
* @returns {number} Total price in cents
*/
function calculateTotalPrice(booking: any) {
let total = 0;
// Sum flight prices
for (const flight of booking.flights) {
total += flight.price || 0;
}
// Sum hotel prices
for (const hotel of booking.hotels) {
total += hotel.price || 0;
}
// Sum car rental prices
for (const car of booking.cars) {
total += car.totalPrice || 0;
}
return total;
}
export default {
searchFlights,
bookFlight
};

290
src/tools/hotels.ts Normal file
View File

@@ -0,0 +1,290 @@
/**
* Hotel Search and Booking Tools
* Implements searchHotels and bookHotel MCP tools
*/
import { getHotelsByCity, getHotelById, generateHotelPrice } from '../data/hotels.js';
import { generatePNR } from '../data/pnr.js';
import { storage } from '../session/storage.js';
import { logger } from '../utils/logger.js';
import { ValidationError, BookingError } from '../utils/errors.js';
import {
validateRequired,
validateString,
validateNumber,
validateFutureDate
} from '../validation/validators.js';
/**
* Search for available hotels
* @param {Object} params - Search parameters
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} Search results
*/
export async function searchHotels(params: any, sessionId: any) {
try {
// Validate required fields
validateRequired(params.cityCode, 'cityCode');
validateRequired(params.checkInDate, 'checkInDate');
validateRequired(params.checkOutDate, 'checkOutDate');
validateString(params.cityCode, 'cityCode', { minLength: 3, maxLength: 3 });
validateFutureDate(params.checkInDate, 'checkInDate');
validateFutureDate(params.checkOutDate, 'checkOutDate');
if (params.guests) {
validateNumber(params.guests, 'guests', { min: 1, max: 10 });
}
const cityCode = params.cityCode.toUpperCase();
const checkInDate = new Date(params.checkInDate);
const checkOutDate = new Date(params.checkOutDate);
const guests = params.guests || 1;
// Validate date range
if (checkOutDate <= checkInDate) {
throw new ValidationError('checkOutDate must be after checkInDate');
}
const checkInDateObj = new Date(checkInDate);
const checkOutDateObj = new Date(checkOutDate);
const nights = Math.ceil((checkOutDateObj.getTime() - checkInDateObj.getTime()) / (1000 * 60 * 60 * 24));
if (nights < 1) {
throw new ValidationError('Minimum stay is 1 night');
}
// Get hotels for city
const cityHotels = getHotelsByCity(cityCode);
if (cityHotels.length === 0) {
return {
cityCode,
checkInDate: params.checkInDate,
checkOutDate: params.checkOutDate,
nights,
results: [],
message: `No hotels found for city code ${cityCode}`
};
}
// Select 5-10 hotels and generate pricing
const numResults = Math.min(cityHotels.length, Math.floor(Math.random() * 6) + 5);
const selectedHotels = cityHotels
.sort(() => Math.random() - 0.5)
.slice(0, numResults);
const results = selectedHotels.map(hotel => {
const pricing = generateHotelPrice(hotel.basePrice, nights, params.checkInDate);
return {
hotelId: hotel.id,
name: hotel.name,
chain: hotel.chain,
stars: hotel.stars,
address: hotel.address,
amenities: hotel.amenities,
pricing: {
pricePerNight: pricing.pricePerNight,
nights: pricing.nights,
totalPrice: pricing.totalPrice,
currency: 'USD'
},
availability: 'available' // Mock: always available
};
});
logger.info({ sessionId, cityCode, nights, resultCount: results.length }, 'Hotel search completed');
return {
cityCode,
checkInDate: params.checkInDate,
checkOutDate: params.checkOutDate,
nights,
guests,
results
};
} catch (error) {
logger.error({ error, sessionId }, 'Hotel search failed');
throw error;
}
}
/**
* Book a hotel
* @param {Object} params - Booking parameters
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} Booking confirmation
*/
export async function bookHotel(params: any, sessionId: any) {
try {
// Validate required fields
validateRequired(params.hotelId, 'hotelId');
validateRequired(params.checkInDate, 'checkInDate');
validateRequired(params.checkOutDate, 'checkOutDate');
validateRequired(params.guestName, 'guestName');
validateRequired(params.guestEmail, 'guestEmail');
const { hotelId, checkInDate, checkOutDate, guestName, guestEmail, guestPhone, pnr: existingPnr, roomType } = params;
// Validate hotel exists
const hotel = getHotelById(hotelId);
if (!hotel) {
throw new ValidationError(`Hotel not found: ${hotelId}`);
}
// Validate dates
validateFutureDate(checkInDate, 'checkInDate');
validateFutureDate(checkOutDate, 'checkOutDate');
const checkIn = new Date(checkInDate);
const checkOut = new Date(checkOutDate);
if (checkOut <= checkIn) {
throw new ValidationError('checkOutDate must be after checkInDate');
}
const checkInDateObj = new Date(params.checkInDate);
const checkOutDateObj = new Date(params.checkOutDate);
const nights = Math.ceil((checkOutDateObj.getTime() - checkInDateObj.getTime()) / (1000 * 60 * 60 * 24));
// Calculate pricing
const pricing = generateHotelPrice(hotel.basePrice, nights, checkInDate);
// Generate or use existing PNR
const pnr = existingPnr || generatePNR();
const client = storage.client;
// Check if this is adding to existing booking
let booking;
if (existingPnr) {
const existingKey = `gds:session:${sessionId}:booking:${existingPnr}`;
const existingData = await client.get(existingKey);
if (!existingData) {
throw new BookingError(`Booking not found: ${existingPnr}`);
}
booking = JSON.parse(existingData);
// Add hotel segment
if (!booking.hotels) {
booking.hotels = [];
}
booking.hotels.push({
hotelId: hotel.id,
name: hotel.name,
chain: hotel.chain,
stars: hotel.stars,
address: hotel.address,
checkInDate,
checkOutDate,
nights,
roomType: roomType || 'Standard',
guestName,
guestEmail,
guestPhone,
pricing: {
pricePerNight: pricing.pricePerNight,
nights: pricing.nights,
totalPrice: pricing.totalPrice,
currency: 'USD'
},
status: 'confirmed',
bookedAt: new Date().toISOString()
});
// Recalculate total price
let totalPrice = 0;
if (booking.flights) {
totalPrice += booking.flights.reduce((sum, f) => sum + (f.pricing?.totalPrice || 0), 0);
}
if (booking.hotels) {
totalPrice += booking.hotels.reduce((sum, h) => sum + (h.pricing?.totalPrice || 0), 0);
}
if (booking.cars) {
totalPrice += booking.cars.reduce((sum, c) => sum + (c.pricing?.totalPrice || 0), 0);
}
booking.totalPrice = totalPrice;
booking.updatedAt = new Date().toISOString();
} else {
// Create new hotel-only booking
booking = {
pnr,
type: 'hotel',
status: 'confirmed',
hotels: [{
hotelId: hotel.id,
name: hotel.name,
chain: hotel.chain,
stars: hotel.stars,
address: hotel.address,
checkInDate,
checkOutDate,
nights,
roomType: roomType || 'Standard',
guestName,
guestEmail,
guestPhone,
pricing: {
pricePerNight: pricing.pricePerNight,
nights: pricing.nights,
totalPrice: pricing.totalPrice,
currency: 'USD'
},
status: 'confirmed',
bookedAt: new Date().toISOString()
}],
totalPrice: pricing.totalPrice,
currency: 'USD',
createdAt: new Date().toISOString(),
sessionId
};
}
// Store booking
const bookingKey = `gds:session:${sessionId}:booking:${pnr}`;
await client.set(bookingKey, JSON.stringify(booking), 'EX', 3600);
// Update session bookings set
await client.sadd(`gds:session:${sessionId}:bookings`, pnr);
logger.info({ sessionId, pnr, hotelId, nights }, 'Hotel booking created');
return {
pnr,
status: 'confirmed',
hotel: {
hotelId: hotel.id,
name: hotel.name,
chain: hotel.chain,
stars: hotel.stars,
checkInDate,
checkOutDate,
nights,
roomType: roomType || 'Standard'
},
pricing: {
pricePerNight: pricing.pricePerNight,
nights: pricing.nights,
totalPrice: pricing.totalPrice,
currency: 'USD'
},
guestName,
guestEmail,
message: existingPnr
? `Hotel added to existing booking ${pnr}`
: `Hotel booking confirmed with PNR ${pnr}`
};
} catch (error) {
logger.error({ error, sessionId }, 'Hotel booking failed');
throw error;
}
}
export default { searchHotels, bookHotel };

161
src/tools/session.ts Normal file
View File

@@ -0,0 +1,161 @@
import { storage } from '../session/storage.js';
import { sessionManager } from '../session/manager.js';
import { logger } from '../utils/logger.js';
/**
* Get session information
* @param {Object} params - Parameters
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} Session information
*/
export async function getSessionInfo(params: any, sessionId: any) {
try {
// Get session data
const session = await sessionManager.getSession(sessionId);
// Calculate uptime
const now = Date.now();
const uptimeMs = now - session.createdAt;
const uptime = formatUptime(uptimeMs);
// Get booking count from session bookings set
const bookingsSetKey = sessionManager.getBookingsKey(sessionId);
const pnrs = await storage.smembers(bookingsSetKey);
const totalBookings = pnrs.length;
// Calculate time until expiry
const ttlSeconds = Math.max(0, Math.floor((session.expiresAt - now) / 1000));
logger.info({
type: 'session_info_retrieved',
sessionId,
bookingCount: totalBookings,
uptime
});
return {
success: true,
session: {
sessionId,
createdAt: new Date(session.createdAt).toISOString(),
lastActivity: new Date(session.lastActivity).toISOString(),
expiresAt: new Date(session.expiresAt).toISOString(),
ttlSeconds,
uptime,
uptimeMs,
bookingCount: totalBookings,
searchCount: session.searchCount
},
metadata: {
timestamp: new Date().toISOString()
}
};
} catch (error) {
logger.error({ error, sessionId }, 'Failed to retrieve session info');
throw error;
}
}
/**
* Clear session data (remove all bookings)
* @param {Object} params - Parameters
* @param {string} sessionId - Session identifier
* @returns {Promise<Object>} Clear confirmation
*/
export async function clearSession(params: any, sessionId: any) {
try {
// Validate confirmation parameter
const confirmed = params.confirm === true;
if (!confirmed) {
return {
success: false,
message: 'Session clear requires confirmation. Set confirm=true to proceed.',
warning: 'This will delete all bookings in the current session.',
metadata: {
sessionId,
timestamp: new Date().toISOString()
}
};
}
// Get all booking PNRs for this session
const bookingsSetKey = sessionManager.getBookingsKey(sessionId);
const pnrs = await storage.smembers(bookingsSetKey);
let deletedCount = 0;
// Delete all bookings from both session-scoped and global storage
for (const pnr of pnrs) {
// Delete from session-scoped storage
const sessionBookingKey = sessionManager.getBookingKey(sessionId, pnr);
await storage.del(sessionBookingKey);
// Delete from global storage
const globalBookingKey = `gds:pnr:${pnr}`;
await storage.del(globalBookingKey);
deletedCount++;
}
// Clear the bookings set
await storage.del(bookingsSetKey);
// Reset booking count
const sessionKey = sessionManager.getSessionKey(sessionId);
await storage.hset(sessionKey, 'bookingCount', '0');
// Preserve sessionId and createdAt (don't delete session itself)
logger.info({
type: 'session_cleared',
sessionId,
deletedBookings: deletedCount
});
return {
success: true,
message: `Session cleared successfully. ${deletedCount} booking(s) deleted.`,
deletedBookings: deletedCount,
preservedData: {
sessionId,
createdAt: 'preserved',
searchCount: 'preserved'
},
metadata: {
sessionId,
timestamp: new Date().toISOString()
}
};
} catch (error) {
logger.error({ error, sessionId }, 'Failed to clear session');
throw error;
}
}
/**
* Format uptime duration as human-readable string
* @param {number} ms - Milliseconds
* @returns {string} Formatted uptime
*/
function formatUptime(ms: any) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h ${minutes % 60}m ${seconds % 60}s`;
} else if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
}
export default {
getSessionInfo,
clearSession
};

15
src/transports/factory.ts Normal file
View File

@@ -0,0 +1,15 @@
import { createHTTPServer } from './http-server.js';
import { logger } from '../utils/logger.js';
/**
* Transport factory - creates Streamable HTTP transport
* HTTP/1.1 + SSE per MCP specification 2025-06-18
*/
export async function createTransport(mcpServer, _options = {}) {
logger.info({ transport: 'http' }, 'Creating MCP Streamable HTTP transport');
// Always use Streamable HTTP (HTTP/1.1 + SSE per MCP specification 2025-06-18)
return await createHTTPServer(mcpServer);
}
export default { createTransport };

View File

@@ -0,0 +1,232 @@
import { logger } from '../utils/logger.js';
import { corsMiddleware } from '../middleware/cors.js';
import { rateLimitMiddleware } from '../middleware/rate-limit.js';
import { protocolVersionMiddleware } from '../middleware/protocol-version.js';
import { requestLoggerMiddleware } from '../middleware/logger.js';
import { messageNormalizationMiddleware } from '../middleware/message-normalization.js';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { isInitializeRequest } from '@modelcontextprotocol/server';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import { ValkeyEventStore } from '../session/valkey-event-store.js';
import { randomUUID } from 'node:crypto';
import type { Request, Response } from 'express';
const PORT = parseInt(process.env.PORT || '3000', 10);
const HOST = process.env.HOST || '127.0.0.1';
/**
* Create and configure Streamable HTTP server per MCP specification 2025-06-18
* Transport: HTTP/1.1 + Server-Sent Events (SSE)
*
* Uses STATELESS mode to support multiple concurrent client initializations.
* Each client can send their own initialize request without server-side session tracking.
*/
export async function createHTTPServer(mcpServer: any) {
const app = createMcpExpressApp();
const transports: Record<string, any> = {};
// Apply CORS globally
app.use(corsMiddleware);
// Health check endpoint - NO auth required
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
service: 'gds-mock-mcp',
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
const mcpPostHandler = async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (sessionId) {
logger.debug({ sessionId, body: req.body }, 'Received MCP request for session');
} else {
logger.info('No session ID provided in MCP request');
}
try {
let transport: NodeStreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
const eventStore = new ValkeyEventStore();
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore, // Enable resumability
onsessioninitialized: sessionId => {
// Store the transport by session ID when session is initialized
// This avoids race conditions where requests might come in before the session is stored
logger.info(`Session initialized with ID: ${sessionId}`);
transports[sessionId] = transport;
}
});
// Set up onclose handler to clean up transport when closed
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
logger.info(`Transport closed for session ${sid}, removing from transports map`);
delete transports[sid];
}
};
// Set up transport error handler to log SDK errors
transport.onerror = (error) => {
console.error('Transport error:', error);
logger.error(
{
error: error.message,
stack: error.stack
},
'MCP Transport error'
);
};
// Connect the transport to the MCP server BEFORE handling the request
// so responses can flow back through the same transport
const server = mcpServer;
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
return; // Already handled
} else if (sessionId) {
res.status(404).json({
jsonrpc: '2.0',
error: { code: -32_001, message: 'Session not found' },
id: null
});
return;
} else {
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32_000, message: 'Bad Request: Session ID required' },
id: null
});
return;
}
// Handle the request with existing transport - no need to reconnect
// The existing transport is already connected to the server
await transport.handleRequest(req, res, req.body);
} catch (error) {
logger.error({ error }, 'Error handling MCP request:');
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32_603,
message: 'Internal server error'
},
id: null
});
}
}
};
app.post('/mcp', mcpPostHandler);
// Handle GET requests for SSE streams (using built-in support from StreamableHTTP)
const mcpGetHandler = async (req, res) => {
const sessionId = req.headers['mcp-session-id'];
if (!sessionId) {
res.status(400).send('Missing session ID');
return;
}
if (!transports[sessionId]) {
res.status(404).send('Session not found');
return;
}
// Check for Last-Event-ID header for resumability
const lastEventId = req.headers['last-event-id'];
if (lastEventId) {
logger.info(`Client reconnecting with Last-Event-ID: ${lastEventId}`);
} else {
logger.info(`Establishing new SSE stream for session ${sessionId}`);
}
const transport = transports[sessionId];
await transport.handleRequest(req, res);
};
app.get('/mcp', mcpGetHandler);
// Handle DELETE requests for session termination (according to MCP spec)
const mcpDeleteHandler = async (req, res) => {
const sessionId = req.headers['mcp-session-id'];
if (!sessionId) {
res.status(400).send('Missing session ID');
return;
}
if (!transports[sessionId]) {
res.status(404).send('Session not found');
return;
}
logger.info(`Received session termination request for session ${sessionId}`);
try {
const transport = transports[sessionId];
await transport.handleRequest(req, res);
} catch (error) {
logger.error(
{ error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined },
'Error handling session termination:'
);
if (!res.headersSent) {
res.status(500).send('Error processing session termination');
}
}
};
app.delete('/mcp', mcpDeleteHandler);
// Handle server shutdown
process.on('SIGINT', async () => {
logger.info('Shutting down server...');
// Close all active transports to properly clean up resources
for (const sessionId in transports) {
try {
logger.info(`Closing transport for session ${sessionId}`);
await transports[sessionId].close();
delete transports[sessionId];
} catch (error) {
logger.error(
{ sessionId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined },
`Error closing transport for session ${sessionId}`
);
}
}
logger.info('Server shutdown complete');
process.exit(0);
});
// MCP endpoint with middleware - applied to /mcp route specifically
// Note: express.json() already parsed the body, so req.body is available
app.use('/mcp', requestLoggerMiddleware);
app.use('/mcp', messageNormalizationMiddleware); // Ensure params field exists
app.use('/mcp', protocolVersionMiddleware);
app.use('/mcp', rateLimitMiddleware);
// Start HTTP server
const httpServer = app.listen(PORT, HOST, () => {
logger.info({ host: HOST, port: PORT }, 'Streamable HTTP server listening');
});
// Graceful shutdown handler
const shutdown = async () => {
logger.info('Shutting down HTTP server...');
httpServer.close(() => {
logger.info('HTTP server closed');
});
};
return { httpServer, shutdown };
}
export default { createHTTPServer };

203
src/utils/errors.ts Normal file
View File

@@ -0,0 +1,203 @@
/**
* MCP Error Codes and Error Handling Utilities
* Based on JSON-RPC 2.0 and MCP specification
*/
/**
* Standard MCP/JSON-RPC 2.0 error codes
*/
export const ErrorCode = {
// JSON-RPC 2.0 standard errors
PARSE_ERROR: -32700,
INVALID_REQUEST: -32600,
METHOD_NOT_FOUND: -32601,
INVALID_PARAMS: -32602,
INTERNAL_ERROR: -32603,
// MCP-specific errors (custom range -32000 to -32099)
INVALID_SESSION: -32001,
SESSION_EXPIRED: -32002,
INVALID_AIRPORT_CODE: -32003,
INVALID_DATE: -32004,
BOOKING_NOT_FOUND: -32005,
BOOKING_CANCELLED: -32006,
INVALID_PASSENGER: -32007,
STORAGE_ERROR: -32008,
VALIDATION_ERROR: -32009,
INVALID_PNR_FORMAT: -32010
};
/**
* Base MCP Error class
*/
export class MCPError extends Error {
code: number;
data: any;
constructor(message: string, code: number, data: any = null) {
super(message);
this.name = 'MCPError';
this.code = code;
this.data = data;
Error.captureStackTrace(this, this.constructor);
}
toJSON() {
return {
code: this.code,
message: this.message,
data: this.data
};
}
}
/**
* Validation Error - Invalid input parameters
*/
export class ValidationError extends MCPError {
constructor(message, data = null) {
super(message, ErrorCode.VALIDATION_ERROR, data);
this.name = 'ValidationError';
}
}
/**
* Session Error - Invalid or expired session
*/
export class SessionError extends MCPError {
constructor(message, expired = false) {
const code = expired ? ErrorCode.SESSION_EXPIRED : ErrorCode.INVALID_SESSION;
super(message, code);
this.name = 'SessionError';
}
}
/**
* Booking Error - Booking not found or already cancelled
*/
export class BookingError extends MCPError {
constructor(message, cancelled = false) {
const code = cancelled ? ErrorCode.BOOKING_CANCELLED : ErrorCode.BOOKING_NOT_FOUND;
super(message, code);
this.name = 'BookingError';
}
}
/**
* Storage Error - Valkey/database operation failed
*/
export class StorageError extends MCPError {
constructor(message, data = null) {
super(message, ErrorCode.STORAGE_ERROR, data);
this.name = 'StorageError';
}
}
/**
* Format error response for MCP
* @param {Error} error - Error object
* @returns {Object} MCP error response
*/
export function formatErrorResponse(error: any) {
if (error instanceof MCPError) {
return {
error: {
code: error.code,
message: error.message,
data: error.data
}
};
}
// Fallback for unknown errors
return {
error: {
code: ErrorCode.INTERNAL_ERROR,
message: error.message || 'Internal server error',
data: {
type: error.name,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
}
}
};
}
/**
* Validate airport code (3-letter IATA)
* @param {string} code - Airport code
* @throws {ValidationError} If code is invalid
*/
export function validateAirportCode(code: any) {
if (!code || typeof code !== 'string' || !/^[A-Z]{3}$/.test(code)) {
throw new ValidationError(
`Invalid airport code '${code}': must be 3-letter IATA code (e.g., JFK, LAX)`,
{ code, expected: 'XXX' }
);
}
}
/**
* Validate date format (ISO 8601: YYYY-MM-DD)
* @param {string} date - Date string
* @param {string} fieldName - Field name for error messages
* @throws {ValidationError} If date is invalid
*/
export function validateDate(date, fieldName = 'date') {
if (!date || typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
throw new ValidationError(
`Invalid ${fieldName} '${date}': must be ISO 8601 date (YYYY-MM-DD)`,
{ date, expected: 'YYYY-MM-DD' }
);
}
const parsed = new Date(date);
if (isNaN(parsed.getTime())) {
throw new ValidationError(
`Invalid ${fieldName} '${date}': not a valid date`,
{ date }
);
}
}
/**
* Validate email format
* @param {string} email - Email address
* @throws {ValidationError} If email is invalid
*/
export function validateEmail(email: any) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email || !emailRegex.test(email)) {
throw new ValidationError(
`Invalid email address '${email}'`,
{ email }
);
}
}
/**
* Validate PNR format (TEST-XXXXXX)
* @param {string} pnr - PNR code
* @throws {ValidationError} If PNR format is invalid
*/
export function validatePNR(pnr: any) {
if (!pnr || typeof pnr !== 'string' || !/^TEST-[A-Z0-9]{6}$/.test(pnr)) {
throw new ValidationError(
`Invalid PNR format '${pnr}': must be TEST-XXXXXX (e.g., TEST-ABC123)`,
{ pnr, expected: 'TEST-XXXXXX' }
);
}
}
export default {
ErrorCode,
MCPError,
ValidationError,
SessionError,
BookingError,
StorageError,
formatErrorResponse,
validateAirportCode,
validateDate,
validateEmail,
validatePNR
};

89
src/utils/logger.ts Normal file
View File

@@ -0,0 +1,89 @@
import pino from 'pino';
/**
* Configure and export Pino structured logger
* LOG_LEVEL env var: silent, error, warn, info, debug, trace (default: info)
*/
const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
export const logger = pino({
level: LOG_LEVEL,
transport: {
target: 'pino/file',
options: {
destination: process.stdout.fd
}
},
formatters: {
level: (label) => {
return { level: label.toUpperCase() };
}
},
timestamp: pino.stdTimeFunctions.isoTime,
base: {
service: 'gds-mock-mcp',
environment: process.env.NODE_ENV || 'development'
}
});
/**
* Create child logger with additional context
* @param {Object} bindings - Additional context fields
* @returns {Object} Child logger instance
*/
export function createLogger(bindings: any) {
return logger.child(bindings);
}
/**
* Log MCP tool call
* @param {string} tool - Tool name
* @param {Object} params - Tool parameters
* @param {string} sessionId - Session identifier
*/
export function logToolCall(tool: any, params: any, sessionId: any) {
logger.info({
type: 'tool_call',
tool,
sessionId,
params
});
}
/**
* Log MCP tool response
* @param {string} tool - Tool name
* @param {number} duration - Response time in ms
* @param {string} sessionId - Session identifier
* @param {boolean} success - Whether operation succeeded
* @param {Object} metadata - Additional response metadata
*/
export function logToolResponse(tool, duration, sessionId, success, metadata = {}) {
logger.info({
type: 'tool_response',
tool,
sessionId,
duration,
success,
...metadata
});
}
/**
* Log error with context
* @param {Error} error - Error object
* @param {Object} context - Error context
*/
export function logError(error, context = {}) {
logger.error({
type: 'error',
error: {
message: error.message,
stack: error.stack,
code: error.code
},
...context
});
}
export default logger;

322
src/validation/schemas.ts Normal file
View File

@@ -0,0 +1,322 @@
/**
* MCP Tool JSON Schemas
* Based on contracts/mcp-tools.md
*/
export const searchFlightsSchema = {
type: 'object',
properties: {
origin: {
type: 'string',
pattern: '^[A-Z]{3}$',
description: 'Origin airport IATA code (3 letters, e.g., JFK)'
},
destination: {
type: 'string',
pattern: '^[A-Z]{3}$',
description: 'Destination airport IATA code (3 letters, e.g., LAX)'
},
departureDate: {
type: 'string',
format: 'date',
description: 'Departure date in ISO 8601 format (YYYY-MM-DD)'
},
passengers: {
type: 'object',
properties: {
adults: {
type: 'integer',
minimum: 1,
maximum: 9,
default: 1
},
children: {
type: 'integer',
minimum: 0,
maximum: 9,
default: 0
},
infants: {
type: 'integer',
minimum: 0,
maximum: 9,
default: 0
}
},
required: ['adults']
},
cabin: {
type: 'string',
enum: ['economy', 'premium_economy', 'business', 'first'],
default: 'economy'
}
},
required: ['origin', 'destination', 'departureDate']
};
export const bookFlightSchema = {
type: 'object',
properties: {
flightId: {
type: 'string',
description: 'Flight identifier from search results'
},
passengers: {
type: 'array',
minItems: 1,
items: {
type: 'object',
properties: {
firstName: {
type: 'string',
minLength: 1,
maxLength: 50,
pattern: '^[A-Za-z\\s\\-]+$'
},
lastName: {
type: 'string',
minLength: 1,
maxLength: 50,
pattern: '^[A-Za-z\\s\\-]+$'
},
type: {
type: 'string',
enum: ['adult', 'child', 'infant']
},
dateOfBirth: {
type: 'string',
format: 'date',
description: 'Date of birth (optional)'
},
email: {
type: 'string',
format: 'email',
description: 'Email address (optional)'
},
phone: {
type: 'string',
description: 'Phone number (optional)'
}
},
required: ['firstName', 'lastName', 'type']
}
},
contactEmail: {
type: 'string',
format: 'email'
},
contactPhone: {
type: 'string'
},
pnr: {
type: 'string',
description: 'Existing PNR to add flight to (optional)'
}
},
required: ['flightId', 'passengers']
};
export const searchHotelsSchema = {
type: 'object',
properties: {
cityCode: {
type: 'string',
pattern: '^[A-Z]{3}$',
description: 'City IATA code (3 letters)'
},
checkInDate: {
type: 'string',
format: 'date'
},
checkOutDate: {
type: 'string',
format: 'date'
},
guests: {
type: 'integer',
minimum: 1,
maximum: 10,
default: 1
},
starRating: {
type: 'integer',
minimum: 1,
maximum: 5,
description: 'Minimum star rating (optional)'
}
},
required: ['cityCode', 'checkInDate', 'checkOutDate']
};
export const bookHotelSchema = {
type: 'object',
properties: {
hotelId: {
type: 'string',
description: 'Hotel identifier from search results'
},
guests: {
type: 'array',
minItems: 1,
items: {
type: 'object',
properties: {
firstName: {
type: 'string',
minLength: 1,
maxLength: 50
},
lastName: {
type: 'string',
minLength: 1,
maxLength: 50
},
email: {
type: 'string',
format: 'email'
}
},
required: ['firstName', 'lastName']
}
},
contactEmail: {
type: 'string',
format: 'email'
},
contactPhone: {
type: 'string'
},
pnr: {
type: 'string',
description: 'Existing PNR to add hotel to (optional)'
}
},
required: ['hotelId', 'guests']
};
export const searchCarsSchema = {
type: 'object',
properties: {
pickupLocation: {
type: 'string',
pattern: '^[A-Z]{3}$',
description: 'Pickup location airport code'
},
pickupDate: {
type: 'string',
format: 'date-time'
},
dropoffLocation: {
type: 'string',
pattern: '^[A-Z]{3}$',
description: 'Dropoff location airport code'
},
dropoffDate: {
type: 'string',
format: 'date-time'
},
vehicleClass: {
type: 'string',
enum: ['economy', 'compact', 'midsize', 'fullsize', 'suv', 'luxury'],
description: 'Preferred vehicle class (optional)'
}
},
required: ['pickupLocation', 'pickupDate', 'dropoffLocation', 'dropoffDate']
};
export const bookCarSchema = {
type: 'object',
properties: {
carId: {
type: 'string',
description: 'Car rental identifier from search results'
},
driver: {
type: 'object',
properties: {
firstName: {
type: 'string',
minLength: 1,
maxLength: 50
},
lastName: {
type: 'string',
minLength: 1,
maxLength: 50
},
email: {
type: 'string',
format: 'email'
},
phone: {
type: 'string'
},
licenseNumber: {
type: 'string',
description: 'Driver license number (optional)'
}
},
required: ['firstName', 'lastName']
},
pnr: {
type: 'string',
description: 'Existing PNR to add car to (optional)'
}
},
required: ['carId', 'driver']
};
export const retrieveBookingSchema = {
type: 'object',
properties: {
pnr: {
type: 'string',
pattern: '^TEST-[A-Z0-9]{6}$',
description: 'Booking reference (e.g., TEST-ABC123)'
}
},
required: ['pnr']
};
export const cancelBookingSchema = {
type: 'object',
properties: {
pnr: {
type: 'string',
pattern: '^TEST-[A-Z0-9]{6}$',
description: 'Booking reference to cancel'
}
},
required: ['pnr']
};
export const listBookingsSchema = {
type: 'object',
properties: {
limit: {
type: 'integer',
minimum: 1,
maximum: 100,
default: 10,
description: 'Maximum number of bookings to return'
},
offset: {
type: 'integer',
minimum: 0,
default: 0,
description: 'Number of bookings to skip'
}
}
};
export default {
searchFlightsSchema,
bookFlightSchema,
searchHotelsSchema,
bookHotelSchema,
searchCarsSchema,
bookCarSchema,
retrieveBookingSchema,
cancelBookingSchema,
listBookingsSchema
};

View File

@@ -0,0 +1,194 @@
import { ValidationError } from '../utils/errors.js';
/**
* JSON Schema Validators using native JavaScript validation
* Based on contracts/mcp-tools.md specifications
*/
/**
* Validate required fields presence
* @param {Object} obj - Object to validate
* @param {string[]} requiredFields - Array of required field names
* @throws {ValidationError} If any required field is missing
*/
export function validateRequired(obj: any, requiredFields: any) {
for (const field of requiredFields) {
if (obj[field] === undefined || obj[field] === null || obj[field] === '') {
throw new ValidationError(`Missing required field: ${field} in ${JSON.stringify(obj)}`);
}
}
}
/**
* Validate string field
* @param {*} value - Value to validate
* @param {string} fieldName - Field name for error messages
* @param {number} minLength - Minimum length (optional)
* @param {number} maxLength - Maximum length (optional)
* @param {RegExp} pattern - Pattern to match (optional)
* @throws {ValidationError} If validation fails
*/
export function validateString(value, fieldName, minLength = null, maxLength = null, pattern = null) {
if (typeof value !== 'string') {
throw new ValidationError(`Field '${fieldName}' must be a string`);
}
if (minLength !== null && value.length < minLength) {
throw new ValidationError(`Field '${fieldName}' must be at least ${minLength} characters`);
}
if (maxLength !== null && value.length > maxLength) {
throw new ValidationError(`Field '${fieldName}' must be at most ${maxLength} characters`);
}
if (pattern !== null && !pattern.test(value)) {
throw new ValidationError(`Field '${fieldName}' does not match required pattern`);
}
}
/**
* Validate number field
* @param {*} value - Value to validate
* @param {string} fieldName - Field name for error messages
* @param {number} min - Minimum value (optional)
* @param {number} max - Maximum value (optional)
* @throws {ValidationError} If validation fails
*/
export function validateNumber(value, fieldName, min = null, max = null) {
if (typeof value !== 'number' || isNaN(value)) {
throw new ValidationError(`Field '${fieldName}' must be a number`);
}
if (min !== null && value < min) {
throw new ValidationError(`Field '${fieldName}' must be at least ${min}`);
}
if (max !== null && value > max) {
throw new ValidationError(`Field '${fieldName}' must be at most ${max}`);
}
}
/**
* Validate enum field
* @param {*} value - Value to validate
* @param {string} fieldName - Field name for error messages
* @param {string[]} allowedValues - Array of allowed values
* @throws {ValidationError} If validation fails
*/
export function validateEnum(value: any, fieldName: any, allowedValues: any) {
if (!allowedValues.includes(value)) {
throw new ValidationError(
`Field '${fieldName}' must be one of: ${allowedValues.join(', ')}. Got: ${value}`
);
}
}
/**
* Validate array field
* @param {*} value - Value to validate
* @param {string} fieldName - Field name for error messages
* @param {number} minItems - Minimum number of items (optional)
* @param {number} maxItems - Maximum number of items (optional)
* @throws {ValidationError} If validation fails
*/
export function validateArray(value, fieldName, minItems = null, maxItems = null) {
if (!Array.isArray(value)) {
throw new ValidationError(`Field '${fieldName}' must be an array`);
}
if (minItems !== null && value.length < minItems) {
throw new ValidationError(`Field '${fieldName}' must have at least ${minItems} items`);
}
if (maxItems !== null && value.length > maxItems) {
throw new ValidationError(`Field '${fieldName}' must have at most ${maxItems} items`);
}
}
/**
* Validate date comparison (date1 < date2)
* @param {string} date1 - First date (ISO 8601)
* @param {string} date2 - Second date (ISO 8601)
* @param {string} field1Name - First field name
* @param {string} field2Name - Second field name
* @throws {ValidationError} If date1 >= date2
*/
export function validateDateOrder(date1: any, date2: any, field1Name: any, field2Name: any) {
const d1 = new Date(date1);
const d2 = new Date(date2);
if (d1 >= d2) {
throw new ValidationError(
`${field1Name} must be before ${field2Name} (${date1} >= ${date2})`
);
}
}
/**
* Validate date is not in the past
* @param {string} date - Date to validate (ISO 8601)
* @param {string} fieldName - Field name for error messages
* @throws {ValidationError} If date is in the past
*/
export function validateFutureDate(date: any, fieldName: any) {
const inputDate = new Date(date);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (inputDate < today) {
throw new ValidationError(
`${fieldName} cannot be in the past (${date})`
);
}
}
/**
* Validate passenger object
* @param {Object} passenger - Passenger data
* @throws {ValidationError} If validation fails
*/
export function validatePassenger(passenger: any) {
validateRequired(passenger, ['firstName', 'lastName', 'type']);
validateString(passenger.firstName, 'firstName', 1, 50, /^[A-Za-z\s\-]+$/);
validateString(passenger.lastName, 'lastName', 1, 50, /^[A-Za-z\s\-]+$/);
validateEnum(passenger.type, 'type', ['adult', 'child', 'infant']);
if (passenger.email) {
validateString(passenger.email, 'email', 5, 100);
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(passenger.email)) {
throw new ValidationError('Invalid email format');
}
}
if (passenger.phone) {
validateString(passenger.phone, 'phone', 10, 20);
}
if (passenger.dateOfBirth) {
validateString(passenger.dateOfBirth, 'dateOfBirth', 10, 10, /^\d{4}-\d{2}-\d{2}$/);
}
}
/**
* Validate cabin class
* @param {string} cabin - Cabin class
* @throws {ValidationError} If validation fails
*/
export function validateCabin(cabin: any) {
const validCabins = ['economy', 'premium_economy', 'business', 'first'];
validateEnum(cabin, 'cabin', validCabins);
}
export default {
validateRequired,
validateString,
validateNumber,
validateEnum,
validateArray,
validateDateOrder,
validateFutureDate,
validatePassenger,
validateCabin
};

155
tests/fixtures/remote-client.js vendored Normal file
View File

@@ -0,0 +1,155 @@
/**
* Example MCP Remote Client
* Demonstrates SSE connection, Last-Event-ID resumption, and tool invocation
*
* Usage: node tests/fixtures/remote-client.js
*/
import { EventSource } from 'eventsource';
const MCP_SERVER_URL = process.env.MCP_SERVER_URL || 'http://127.0.0.1:3000';
const MCP_PROTOCOL_VERSION = '2025-06-18';
/**
* Example: Connect to MCP server and invoke searchFlights tool
*/
async function exampleClient() {
console.log(`Connecting to MCP server at ${MCP_SERVER_URL}...`);
// 1. Initialize connection - send initialize request
// Per MCP spec: MCP-Protocol-Version header is NOT required during initialization
const initRequest = {
jsonrpc: '2.0',
id: 0,
method: 'initialize',
params: {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: {},
clientInfo: {
name: 'gds-mock-test-client',
version: '1.0.0'
}
}
};
console.log('Sending initialize request (without MCP-Protocol-Version header)...');
const initResponse = await fetch(`${MCP_SERVER_URL}/mcp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream'
// Note: MCP-Protocol-Version header NOT included during initialization
},
body: JSON.stringify(initRequest)
});
const initResult = await initResponse.json();
console.log('Server initialized:', JSON.stringify(initResult, null, 2));
// Note: Server runs in stateless mode (no MCP-Session-Id header will be present)
// Each client can initialize independently without server-side session management
const sessionId = initResponse.headers.get('MCP-Session-Id');
if (sessionId) {
console.log('Session ID:', sessionId);
} else {
console.log('Server running in stateless mode (no session ID)');
}
// 2. Open SSE stream for server messages (now with MCP-Protocol-Version header)
const eventSource = new EventSource(`${MCP_SERVER_URL}/mcp`, {
headers: {
'MCP-Protocol-Version': MCP_PROTOCOL_VERSION,
...(sessionId && { 'MCP-Session-Id': sessionId })
}
});
let lastEventId = null;
// Handle SSE events
eventSource.addEventListener('message', (event) => {
console.log('Received SSE event:', event.data);
lastEventId = event.lastEventId;
try {
const data = JSON.parse(event.data);
// Extract session ID from InitializeResult
if (data.result && data.result.protocolVersion) {
console.log('Server initialized:', data.result);
}
} catch {
// Empty data or non-JSON - expected per SSE polling pattern
}
});
eventSource.addEventListener('error', (error) => {
console.error('SSE error:', error);
});
// Wait for connection
await new Promise(resolve => setTimeout(resolve, 1000));
// 2. Send tool invocation via POST
const searchRequest = {
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: {
name: 'searchFlights',
arguments: {
origin: 'JFK',
destination: 'LAX',
departureDate: '2026-05-01',
passengers: 1,
cabin: 'economy'
}
}
};
console.log('Sending searchFlights request...');
const response = await fetch(`${MCP_SERVER_URL}/mcp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'MCP-Protocol-Version': MCP_PROTOCOL_VERSION,
...(sessionId && { 'MCP-Session-Id': sessionId }),
...(lastEventId && { 'Last-Event-ID': lastEventId })
},
body: JSON.stringify(searchRequest)
});
const result = await response.json();
console.log('Search results:', JSON.stringify(result, null, 2));
// 3. Demonstrate Last-Event-ID resumption
if (lastEventId) {
console.log(`\nReconnecting with Last-Event-ID: ${lastEventId}...`);
const resumedEventSource = new EventSource(`${MCP_SERVER_URL}/mcp`, {
headers: {
'MCP-Protocol-Version': MCP_PROTOCOL_VERSION,
'Last-Event-ID': lastEventId
}
});
resumedEventSource.addEventListener('message', (event) => {
console.log('Resumed stream event:', event.data);
});
// Close after demonstration
setTimeout(() => {
resumedEventSource.close();
eventSource.close();
console.log('\nClient demonstration complete');
process.exit(0);
}, 2000);
}
}
// Run example
exampleClient().catch(err => {
console.error('Client error:', err);
process.exit(1);
});

2
tests/unit/.gitkeep Normal file
View File

@@ -0,0 +1,2 @@
# Tests directory placeholder
# Test implementation not included in MVP scope

23
tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": false,
"noImplicitAny": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}