Files

26 KiB

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:

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

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:

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

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

# 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):

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:

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

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

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:

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:

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

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:

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

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:

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)

# 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