# 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 // 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